PGN parsing and writing

Parsing

chess.pgn.read_game(handle, Visitor=<class 'chess.pgn.GameModelCreator'>)

Reads a game from a file opened in text mode.

>>> import chess.pgn
>>>
>>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn")
>>>
>>> first_game = chess.pgn.read_game(pgn)
>>> second_game = chess.pgn.read_game(pgn)
>>>
>>> first_game.headers["Event"]
'IBM Man-Machine, New York USA'
>>>
>>> # Iterate through all moves and play them on a board.
>>> board = first_game.board()
>>> for move in first_game.main_line():
...     board.push(move)
...
>>> board
Board('4r3/6P1/2p2P1k/1p6/pP2p1R1/P1B5/2P2K2/3r4 b - - 0 45')

By using text mode, the parser does not need to handle encodings. It is the caller’s responsibility to open the file with the correct encoding. PGN files are ASCII or UTF-8 most of the time. So, the following should cover most relevant cases (ASCII, UTF-8, UTF-8 with BOM).

>>> # Python 3
>>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn", encoding="utf-8-sig")

Use StringIO to parse games from a string.

>>> pgn_string = "1. e4 e5 2. Nf3 *"
>>>
>>> try:
...     from StringIO import StringIO  # Python 2
... except ImportError:
...     from io import StringIO  # Python 3
...
>>> pgn = StringIO(pgn_string)
>>> game = chess.pgn.read_game(pgn)

The end of a game is determined by a completely blank line or the end of the file. (Of course, blank lines in comments are possible.)

According to the PGN standard, at least the usual 7 header tags are required for a valid game. This parser also handles games without any headers just fine.

The parser is relatively forgiving when it comes to errors. It skips over tokens it can not parse. Any exceptions are logged.

Returns the parsed game or None if the end of file is reached.

Writing

If you want to export your game with all headers, comments and variations, you can do it like this:

>>> import chess
>>> import chess.pgn
>>>
>>> game = chess.pgn.Game()
>>> game.headers["Event"] = "Example"
>>> node = game.add_variation(chess.Move.from_uci("e2e4"))
>>> node = node.add_variation(chess.Move.from_uci("e7e5"))
>>> node.comment = "Comment"
>>>
>>> print(game)
[Event "Example"]
[Site "?"]
[Date "????.??.??"]
[Round "?"]
[White "?"]
[Black "?"]
[Result "*"]

1. e4 e5 { Comment } *

Remember that games in files should be separated with extra blank lines.

>>> print(game, file=open("/dev/null", "w"), end="\n\n")

Use the StringExporter() or FileExporter() visitors if you need more control.

Game model

Games are represented as a tree of moves. Each GameNode can have extra information, such as comments. The root node of a game (Game extends the GameNode) also holds general information, such as game headers.

class chess.pgn.Game(headers=None)

The root node of a game with extra information such as headers and the starting position. Also has all the other properties and methods of GameNode.

headers

A mapping of headers. By default, the following 7 headers are provided:

>>> import chess.pgn
>>>
>>> game = chess.pgn.Game()
>>> game.headers["Event"]
'?'
>>> game.headers["Site"]
'?'
>>> game.headers["Date"]
'????.??.??'
>>> game.headers["Round"]
'?'
>>> game.headers["White"]
'?'
>>> game.headers["Black"]
'?'
>>> game.headers["Result"]
'*'
errors

A list of illegal or ambiguous move errors encountered while parsing the game.

board(_cache=False)

Gets the starting position of the game.

Unless the FEN header tag is set, this is the default starting position (for the Variant).

setup(board)

Setup a specific starting position. This sets (or resets) the FEN, SetUp, and Variant header tags.

accept(visitor)

Traverses the game in PGN order using the given visitor. Returns the visitor result.

classmethod from_board(board)

Creates a game from the move stack of a Board().

classmethod without_tag_roster()

Creates an empty game without the default 7 tag roster.

class chess.pgn.GameNode
parent

The parent node or None if this is the root node of the game.

move

The move leading to this node or None if this is the root node of the game.

nags = set()

A set of NAGs as integers. NAGs always go behind a move, so the root node of the game can have none.

comment = ''

A comment that goes behind the move leading to this node. Comments that occur before any move are assigned to the root node.

starting_comment = ''

A comment for the start of a variation. Only nodes that actually start a variation (starts_variation() checks this) can have a starting comment. The root node can not have a starting comment.

variations

A list of child nodes.

board(_cache=True)

Gets a board with the position of the node.

It’s a copy, so modifying the board will not alter the game.

san()

Gets the standard algebraic notation of the move leading to this node. See chess.Board.san().

Do not call this on the root node.

uci(chess960=None)

Gets the UCI notation of the move leading to this node. See chess.Board.uci().

Do not call this on the root node.

root()

Gets the root node, i.e. the game.

end()

Follows the main variation to the end and returns the last node.

is_end()

Checks if this node is the last node in the current variation.

starts_variation()

Checks if this node starts a variation (and can thus have a starting comment). The root node does not start a variation and can have no starting comment.

is_main_line()

Checks if the node is in the main line of the game.

is_main_variation()

Checks if this node is the first variation from the point of view of its parent. The root node is also in the main variation.

variation(move)

Gets a child node by either the move or the variation index.

has_variation(move)

Checks if the given move appears as a variation.

promote_to_main(move)

Promotes the given move to the main variation.

promote(move)

Moves a variation one up in the list of variations.

demote(move)

Moves a variation one down in the list of variations.

remove_variation(move)

Removes a variation.

add_variation(move, comment='', starting_comment='', nags=())

Creates a child node with the given attributes.

add_main_variation(move, comment='')

Creates a child node with the given attributes and promotes it to the main variation.

main_line()

Yields the moves of the main line starting in this node.

add_line(moves, comment='', starting_comment='', nags=())

Creates a sequence of child nodes for the given list of moves. Adds comment and nags to the last node of the line and returns it.

accept(visitor, _board=None)

Traverse game nodes in PGN order using the given visitor. Returns the visitor result.

accept_subgame(visitor)

Traverses headers and game nodes in PGN order, as if the game was starting from this node. Returns the visitor result.

Visitors

Visitors are an advanced concept for game tree traversal.

class chess.pgn.BaseVisitor

Base class for visitors.

Use with chess.pgn.Game.accept() or chess.pgn.GameNode.accept().

The methods are called in PGN order.

begin_game()

Called at the start of a game.

begin_headers()

Called at the start of game headers.

visit_header(tagname, tagvalue)

Called for each game header.

end_headers()

Called at the end of the game headers.

visit_move(board, move)

Called for each move.

board is the board state before the move. The board state must be restored before the traversal continues.

visit_comment(comment)

Called for each comment.

visit_nag(nag)

Called for each NAG.

begin_variation()

Called at the start of a new variation. It is not called for the main line of the game.

end_variation()

Concludes a variation.

visit_result(result)

Called at the end of a game with the value from the Result header.

end_game()

Called at the end of a game.

result()

Called to get the result of the visitor. Defaults to True.

handle_error(error)

Called for encountered errors. Defaults to raising an exception.

The following visitors are readily available.

class chess.pgn.GameModelCreator

Creates a game model. Default visitor for read_game().

handle_error(error)

Populates chess.pgn.Game.errors with encountered errors and logs them.

result()

Returns the visited Game().

class chess.pgn.StringExporter(columns=80, headers=True, comments=True, variations=True)

Allows exporting a game as a string.

>>> import chess.pgn
>>>
>>> game = chess.pgn.Game()
>>>
>>> exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
>>> pgn_string = game.accept(exporter)

Only columns characters are written per line. If columns is None then the entire movetext will be on a single line. This does not affect header tags and comments.

There will be no newline characters at the end of the string.

class chess.pgn.FileExporter(handle, columns=80, headers=True, comments=True, variations=True)

Acts like a StringExporter, but games are written directly into a text file.

There will always be a blank line after each game. Handling encodings is up to the caller.

>>> import chess.pgn
>>>
>>> game = chess.pgn.Game()
>>>
>>> new_pgn = open("/dev/null", "w", encoding="utf-8")
>>> exporter = chess.pgn.FileExporter(new_pgn)
>>> game.accept(exporter)

NAGs

Numeric anotation glyphs describe moves and positions using standardized codes that are understood by many chess programs. During PGN parsing, annotations like !, ?, !!, etc., are also converted to NAGs.

chess.pgn.NAG_GOOD_MOVE = 1

A good move. Can also be indicated by ! in PGN notation.

chess.pgn.NAG_MISTAKE = 2

A mistake. Can also be indicated by ? in PGN notation.

chess.pgn.NAG_BRILLIANT_MOVE = 3

A brilliant move. Can also be indicated by !! in PGN notation.

chess.pgn.NAG_BLUNDER = 4

A blunder. Can also be indicated by ?? in PGN notation.

chess.pgn.NAG_SPECULATIVE_MOVE = 5

A speculative move. Can also be indicated by !? in PGN notation.

chess.pgn.NAG_DUBIOUS_MOVE = 6

A dubious move. Can also be indicated by ?! in PGN notation.

Skimming

These functions allow for quickly skimming games without fully parsing them.

chess.pgn.scan_headers(handle)

Scan a PGN file opened in text mode for game offsets and headers.

Yields a tuple for each game. The first element is the offset and the second element is a mapping of game headers.

Since actually parsing many games from a big file is relatively expensive, this is a better way to look only for specific games and then seek and parse them later.

This example scans for the first game with Kasparov as the white player.

>>> import chess.pgn
>>>
>>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn")
>>>
>>> for offset, headers in chess.pgn.scan_headers(pgn):
...     if "Kasparov" in headers["White"]:
...         kasparov_offset = offset
...         break

Then it can later be seeked an parsed.

>>> pgn.seek(kasparov_offset)
0
>>> game = chess.pgn.read_game(pgn)

This also works nicely with generators, scanning lazily only when the next offset is required.

>>> white_win_offsets = (offset for offset, headers in chess.pgn.scan_headers(pgn)
...                             if headers["Result"] == "1-0")
>>> first_white_win = next(white_win_offsets)
>>> second_white_win = next(white_win_offsets)
Warning:Be careful when seeking a game in the file while more offsets are being generated.
chess.pgn.scan_offsets(handle)

Scan a PGN file opened in text mode for game offsets.

Yields the starting offsets of all the games, so that they can be seeked later. This is just like scan_headers() but more efficient if you do not actually need the header information.

The PGN standard requires each game to start with an Event tag. So does this scanner.