PGN parsing and writing

Parsing

chess.pgn.read_game(handle: TextIO) Game | None[source]
chess.pgn.read_game(handle: TextIO, *, Visitor: Callable[[], BaseVisitor[ResultT]]) ResultT | None

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.mainline_moves():
...     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 usually ASCII or UTF-8 encoded, sometimes with BOM (which this parser automatically ignores). See open() for options to deal with encoding errors.

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

Use StringIO to parse games from a string.

>>> import io
>>>
>>> pgn = io.StringIO("1. e4 e5 2. Nf3 *")
>>> 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 seven 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. By default, any exceptions are logged and collected in Game.errors. This behavior can be overridden.

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. Conceptually each node represents a position of the game. The tree consists of one root node (Game, also holding game headers) and many child nodes (ChildNode). Both extend GameNode.

Note

Some basic methods have complexity O(n) for a game with n moves. When following a variation, it is often more efficient to use visitors or incrementally update state (like board, ply counter, or turn).

class chess.pgn.GameNode(*, comment: str = '')[source]
parent: GameNode | None

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

move: Move | None

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

variations: List[ChildNode]

A list of child nodes.

comment: str

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

abstract board() Board[source]

Gets a board with the position of the node.

For the root node, this is the default starting position (for the Variant) unless the FEN header tag is set.

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

Complexity is O(n).

abstract ply() int[source]

Returns the number of half-moves up to this node, as indicated by fullmove number and turn of the position. See chess.Board.ply().

Usually this is equal to the number of parent nodes, but it may be more if the game was started from a custom position.

Complexity is O(n).

turn() chess.Color[source]

Gets the color to move at this node. See chess.Board.turn.

Complexity is O(n).

game() Game[source]

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

Complexity is O(n).

end() GameNode[source]

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

Complexity is O(n).

is_end() bool[source]

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

Complexity is O(1).

starts_variation() bool[source]

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.

For example, in 1. e4 e5 (1... c5 2. Nf3) 2. Nf3, the node holding 1… c5 starts a variation.

Complexity is O(1).

is_mainline() bool[source]

Checks if the node is in the mainline of the game.

Complexity is O(n).

is_main_variation() bool[source]

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.

Complexity is O(1).

variation(move: int | Move | GameNode) ChildNode[source]

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

has_variation(move: int | Move | GameNode) bool[source]

Checks if this node has the given variation.

promote_to_main(move: int | Move | GameNode) None[source]

Promotes the given move to the main variation.

promote(move: int | Move | GameNode) None[source]

Moves a variation one up in the list of variations.

demote(move: int | Move | GameNode) None[source]

Moves a variation one down in the list of variations.

remove_variation(move: int | Move | GameNode) None[source]

Removes a variation.

add_variation(move: Move, *, comment: str = '', starting_comment: str = '', nags: Iterable[int] = []) ChildNode[source]

Creates a child node with the given attributes.

add_main_variation(move: Move, *, comment: str = '', nags: Iterable[int] = []) ChildNode[source]

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

next() ChildNode | None[source]

Returns the first node of the mainline after this node, or None if this node does not have any children.

Complexity is O(1).

mainline() Mainline[ChildNode][source]

Returns an iterable over the mainline starting after this node.

mainline_moves() Mainline[Move][source]

Returns an iterable over the main moves after this node.

add_line(moves: Iterable[Move], *, comment: str = '', starting_comment: str = '', nags: Iterable[int] = []) GameNode[source]

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.

eval() PovScore | None[source]

Parses the first valid [%eval ...] annotation in the comment of this node, if any.

Complexity is O(n).

eval_depth() int | None[source]

Parses the first valid [%eval ...] annotation in the comment of this node and returns the corresponding depth, if any.

Complexity is O(1).

set_eval(score: PovScore | None, depth: int | None = None) None[source]

Replaces the first valid [%eval ...] annotation in the comment of this node or adds a new one.

arrows() List[Arrow][source]

Parses all [%csl ...] and [%cal ...] annotations in the comment of this node.

Returns a list of arrows.

set_arrows(arrows: Iterable[Arrow | Tuple[chess.Square, chess.Square]]) None[source]

Replaces all valid [%csl ...] and [%cal ...] annotations in the comment of this node or adds new ones.

clock() float | None[source]

Parses the first valid [%clk ...] annotation in the comment of this node, if any.

Returns the player’s remaining time to the next time control after this move, in seconds.

set_clock(seconds: float | None) None[source]

Replaces the first valid [%clk ...] annotation in the comment of this node or adds a new one.

emt() float | None[source]

Parses the first valid [%emt ...] annotation in the comment of this node, if any.

Returns the player’s elapsed move time use for the comment of this move, in seconds.

set_emt(seconds: float | None) None[source]

Replaces the first valid [%emt ...] annotation in the comment of this node or adds a new one.

abstract accept(visitor: BaseVisitor[ResultT]) ResultT[source]

Traverses game nodes in PGN order using the given visitor. Starts with the move leading to this node. Returns the visitor result.

accept_subgame(visitor: BaseVisitor[ResultT]) ResultT[source]

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

class chess.pgn.Game(headers: Mapping[str, str] | Iterable[Tuple[str, str]] | None = None)[source]

The root node of a game with extra information such as headers and the starting position. Extends GameNode.

headers: Headers

A mapping of headers. By default, the following 7 headers are provided (Seven Tag Roster):

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

A list of errors (such as illegal or ambiguous moves) encountered while parsing the game.

setup(board: Board | str) None[source]

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

accept(visitor: BaseVisitor[ResultT]) ResultT[source]

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

classmethod from_board(board: Board) GameT[source]

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

classmethod without_tag_roster() GameT[source]

Creates an empty game without the default Seven Tag Roster.

class chess.pgn.ChildNode(parent: GameNode, move: Move, *, comment: str = '', starting_comment: str = '', nags: Iterable[int] = [])[source]

A child node of a game, with the move leading to it. Extends GameNode.

nags: Set[int]

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

parent: GameNode

The parent node.

move: Move

The move leading to this node.

starting_comment: str

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.

san() str[source]

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

Do not call this on the root node.

Complexity is O(n).

uci(*, chess960: bool | None = None) str[source]

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

Do not call this on the root node.

Complexity is O(n).

end() ChildNode[source]

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

Complexity is O(n).

Visitors

Visitors are an advanced concept for game tree traversal.

class chess.pgn.BaseVisitor[source]

Base class for visitors.

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

The methods are called in PGN order.

begin_game() SkipType | None[source]

Called at the start of a game.

begin_headers() Headers | None[source]

Called before visiting game headers.

visit_header(tagname: str, tagvalue: str) None[source]

Called for each game header.

end_headers() SkipType | None[source]

Called after visiting game headers.

begin_parse_san(board: Board, san: str) SkipType | None[source]

When the visitor is used by a parser, this is called at the start of each standard algebraic notation detailing a move.

parse_san(board: Board, san: str) Move[source]

When the visitor is used by a parser, this is called to parse a move in standard algebraic notation.

You can override the default implementation to work around specific quirks of your input format.

Deprecated since version 1.1: This method is very limited, because it is only called on moves that the parser recognizes in the first place. Instead of adding workarounds here, please report common quirks so that they can be handled for everyone.

visit_move(board: Board, move: Move) None[source]

Called for each move.

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

visit_board(board: Board) None[source]

Called for the starting position of the game and after each move.

The board state must be restored before the traversal continues.

visit_comment(comment: str) None[source]

Called for each comment.

visit_nag(nag: int) None[source]

Called for each NAG.

begin_variation() SkipType | None[source]

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

end_variation() None[source]

Concludes a variation.

visit_result(result: str) None[source]

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

end_game() None[source]

Called at the end of a game.

abstract result() ResultT[source]

Called to get the result of the visitor.

handle_error(error: Exception) None[source]

Called for encountered errors. Defaults to raising an exception.

The following visitors are readily available.

class chess.pgn.GameBuilder[source]
class chess.pgn.GameBuilder(*, Game: Type[GameT])

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

handle_error(error: Exception) None[source]

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

You can silence the log and handle errors yourself after parsing:

>>> import chess.pgn
>>> import logging
>>>
>>> logging.getLogger("chess.pgn").setLevel(logging.CRITICAL)
>>>
>>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn")
>>>
>>> game = chess.pgn.read_game(pgn)
>>> game.errors  # List of exceptions
[]

You can also override this method to hook into error handling:

>>> import chess.pgn
>>>
>>> class MyGameBuilder(chess.pgn.GameBuilder):
>>>     def handle_error(self, error: Exception) -> None:
>>>         pass  # Ignore error
>>>
>>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn")
>>>
>>> game = chess.pgn.read_game(pgn, Visitor=MyGameBuilder)
result() GameT[source]

Returns the visited Game().

class chess.pgn.HeadersBuilder[source]
class chess.pgn.HeadersBuilder(*, Headers: Type[Headers])

Collects headers into a dictionary.

class chess.pgn.BoardBuilder[source]

Returns the final position of the game. The mainline of the game is on the move stack.

class chess.pgn.SkipVisitor[source]

Skips a game.

class chess.pgn.StringExporter(*, columns: int | None = 80, headers: bool = True, comments: bool = True, variations: bool = True)[source]

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: TextIO, *, columns: int | None = 80, headers: bool = True, comments: bool = True, variations: bool = True)[source]

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.read_headers(handle: TextIO) Headers | None[source]

Reads game headers from a PGN file opened in text mode. Skips the rest of the game.

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")
>>>
>>> kasparov_offsets = []
>>>
>>> while True:
...     offset = pgn.tell()
...
...     headers = chess.pgn.read_headers(pgn)
...     if headers is None:
...         break
...
...     if "Kasparov" in headers.get("White", "?"):
...         kasparov_offsets.append(offset)

Then it can later be seeked and parsed.

>>> for offset in kasparov_offsets:
...     pgn.seek(offset)
...     chess.pgn.read_game(pgn)  
0
<Game at ... ('Garry Kasparov' vs. 'Deep Blue (Computer)', 1997.??.??)>
1436
<Game at ... ('Garry Kasparov' vs. 'Deep Blue (Computer)', 1997.??.??)>
3067
<Game at ... ('Garry Kasparov' vs. 'Deep Blue (Computer)', 1997.??.??)>
chess.pgn.skip_game(handle: TextIO) bool[source]

Skips a game. Returns True if a game was found and skipped.