Use Python to read chess games, player biographies, notable games, tournament archives and annotated books from Classic Chess. The SDK also searches the larger MasterDB game database, retrieves opening statistics, exports games, and calls account and application APIs.
Public reads need no account or API key. The SDK uses the Python standard library and is MIT licensed; see LICENSE.
Install Python 3.10 or newer. In a terminal, create an isolated Python environment and install from PyPI:
python3 -m venv classicchess-env
source classicchess-env/bin/activate
python3 -m pip install classicchess-sdk
classicchess-api public players --namesOn Windows PowerShell, use py instead of python3 and activate with
classicchess-env\Scripts\Activate.ps1 instead of the source command.
The last command prints the names of all curated players. To install from the
public source repository instead, install Git and use
python3 -m pip install git+https://github.com/Andromeda1957/classicchess-api-python.git.
To use it in a Python program, create example.py with this complete example:
from classicchess_api import ClassicChessClient api = ClassicChessClient() print(api.player_names()[:10])
Run python3 example.py in the same activated environment. You should see up to
ten player names. The PyPI distribution is classicchess-sdk; the import is
classicchess_api and the command-line executable is classicchess-api. python3 -m classicchess_api runs the same command-line tool.
If you installed the previous classicchess-api distribution, run
python3 -m pip uninstall classicchess-api before installing classicchess-sdk
in that environment. Both distributions provide the same import and command,
so install only one. Existing Python programs and CLI commands still work.
The MasterDB and public game-list commands accept a nonnegative --limit. --limit 0 returns no
results and makes no per-game PGN requests. This applies to the default single
page and to --all-pages; negative limits are rejected before a request is sent.
The export commands take the same search filters but no --limit, --page,
--page-size, --all-pages or --pgn: each export is one reply of at most 300
games, so those options are refused before a request is sent.
Player slugs are identifiers returned by the API. Use them as returned rather than guessing a slug from a display name. This complete example discovers a player and prints the first ten games in their archive:
from classicchess_api import ClassicChessClient api = ClassicChessClient() players = api.public_players()["results"] if players: player = players[0] print(player["name"]) biography = api.public_player_bio(player["slug"])["bio"] print(biography["lede"] if biography else "Biography unavailable") for game in api.iterate_public_games(archive_player=player["slug"], limit=10): print(game["token"], game["white"], game["black"])
Catalogs return complete lists. Game searches return one page by default.
The iterate_* methods fetch pages as you consume them; breaking the loop or
setting limit stops without fetching an extra page. The all_pages=True
option on game-list methods collects pages into one dictionary instead.
Missing biographies are None, and notable lists can be empty.
| Capability | Python methods |
|---|---|
| API discovery | discovery() |
| Player, event and book catalogs | public_players(), public_events(), annotated_books() |
| Names and titles only | player_names(), event_names(), book_titles() |
| Player cards for lists and pickers | public_player_cards() returns every player with only the biography headline (epithet, lifespan, crown), about a tenth the size of public_players(); player_names() reads it |
| Player profiles, biographies and notable games | public_player(slug), public_player_bio(slug), public_notable_games(slug) |
| Event series | event_series(query=...) or event_series(series=...) |
| Public game lists, detail and PGN | public_games(), public_game(token), public_pgn(token) |
| MasterDB game lists, detail and PGN | master_games(query), master_game(token), master_pgn(token) |
| Annotated game lists, detail and PGN | annotated_games(book_slug), annotated_game(book_slug, game_slug), annotated_pgn(book_slug, game_slug=None) |
| Streaming games | iterate_public_games(), iterate_master_games(query), iterate_annotated_games(book_slug) |
| Combine PGN text for returned games | pgn_text_for_games(games) |
| PGN or NDJSON export | export_public_games(), export_master_games(); select format="pgn" or format="ndjson" |
| Player search and statistics | players(query), master_stats(query=...) or master_stats(player=..., opponent=...) |
| Opening explorer | explorer(play=...) or explorer(fen=...), explorer_sources() |
| Lichess opening explorer | lichess_explorer(fen=..., player=..., color=..., result=...) relays through Classic Chess, so no Lichess token is needed; no player covers the whole Lichess database, a Lichess username covers that player's games |
| Event detail and About page | public_event(event_slug), public_event_about(event_slug) |
| Photo gallery | gallery(query=None, page=1, page_size=48), gallery_photo(photo_id) |
| Beginner games and Game of the Day | beginner_games(), daily_game() |
| Event of the day | event_of_the_day() returns the landmark event on today's homepage, its tagline and pictured winner; event is None when none can be published |
| Today's homepage card | todays_picks() returns the Player of the day, Game of the Day, On this day game and Event of the day in one read, with portraits where available; each is None when absent |
| Site search | site_search(query) for a preview of players, events and games; site_search(query, kind="games", page=2) for one paginated kind |
| Endgame tablebase | tablebase(fen) uses local Syzygy tables and falls back to the Lichess tablebase for larger positions, as the site does |
| Account collections | ApplicationClient.account_me(token), account_collections(token), account_create_collection(name, token), account_import_public_player_games(archive_player, token, ...) |
| Add single games to collections | ApplicationClient.account_add_collection_game(collection_id, game_slug, token) |
| Starred players and games | account_starred_players(token, page=1, page_size=50), account_star_player(slug, token), account_unstar_player(slug, token), account_starred_games(token, ...), account_star_game(slug, token), account_unstar_game(slug, token) |
| Change or delete imported games | account_set_imported_game_visibility(slug, "public" or "private", token), account_delete_imported_game(slug, token) |
| Public imported games | public_imported_game(username, game_slug), public_imported_pgn(username, game_slug) read a game another account imported and made public, by its page address |
| GIF exports | ApplicationClient.master_game_gif(game_token, token), public_game_gif(slug, token), annotated_game_gif(book_slug, game_slug, token), public_imported_game_gif(username, slug, token), account_imported_game_gif(slug, token); add orientation="black" to flip the board |
| Notifications | account_notifications(token, page=1, page_size=50), account_mark_notification_read(id, token), account_mark_all_notifications_read(token), account_dismiss_notification(id, token), account_dismiss_all_notifications(token), account_notification_preferences(token), account_update_notification_preferences(token, topics=..., sound_enabled=...) |
| Notebook exports | account_notebooks(token), account_notebook(uuid, token), account_notebook_chapter_pgn(uuid, chapter_id, token), account_notebook_file(uuid, token, password=None, ccnb_format=None) |
| Annotated Notebook packages | ClassicChessClient.annotated_books(ccnb_format=3), annotated_notebook_file(book_slug, ccnb_format=3) |
| Play the Gym's bots | account_gym(token), account_gym_games(token, page=1, page_size=20), account_gym_new_game(bot_key, token, color="white", time="unlimited"), account_gym_game(id, token), account_gym_move(id, uci, ply, token), account_gym_bot_move(id, ply, token), account_gym_take_back(id, token), account_gym_resign(id, token), account_gym_clock(id, token), account_gym_abort(id, token), account_gym_delete_game(id, token), account_gym_game_pgn(id, token) |
| Puzzles | account_puzzles(token), account_puzzle_next(token, after=None), account_puzzle(id, token), account_puzzle_result(id, "solved" or "missed", token), account_puzzle_history(token, page=1, page_size=20), account_puzzle_reset(token) |
| Notebook, Remote, Cast and other application APIs | ApplicationClient.request(path, method=..., body=..., token=...), download(path, token=...) for files |
| Scanner upload | ApplicationClient.scan_position(jpeg_bytes, token) |
Notebook downloads accept ccnb_format=2 for legacy exchange or ccnb_format=3
for the binary record container. Omission retains legacy behavior. Use the same
format for the annotated catalog and package download so their revisions match.
The public package helper returns exact bytes from the fixed same-origin
endpoint for a validated catalog slug, sends no account credentials, and rejects
redirects. Unsupported values fail before a request. Capabilities unavailable in
legacy exchange require format 3.
Public game filters include query, archive_player, archive_event,
search_mode, since, until, sort, page and page_size. search_mode
chooses which fields query searches: players (players, event, site and
date), openings (opening name and ECO) or all (the default). Each export is capped at 300 games.
For larger archives, iterate games and export batches of up to 300 returned
tokens. NDJSON means one JSON object per line; pgn_in_json=False omits the
PGN field from that format. PGN is the standard text format for chess games.
Public archive reads use ClassicChessClient. For private account or device
operations use ApplicationClient and pass a token explicitly for each request.
Create a personal API token in your signed-in Classic Chess profile settings
with the scopes required by the account API reference.
Set the CLASSICCHESS_API_TOKEN environment variable to that token before
running this read-only account example:
import os from classicchess_api import ApplicationClient api = ApplicationClient() response = api.account_me(os.environ["CLASSICCHESS_API_TOKEN"]) print(response.status, response.data)
response.ok indicates HTTP success. HTTP JSON errors preserve their status,
data and retry_after; network failures and invalid responses raise ApiError.
Application requests accept JSON text or bytes, including binary notebook uploads.
Scanner uploads accept JPEG bytes up to 850,000 bytes. Account tokens and mobile
or Desktop device sessions have different scopes; use the credential required
by the endpoint. Do not embed tokens in source files.
GIF, chapter PGN and Notebook file methods return an ApplicationDownload with
ok, status, content (the file bytes when ok), content_type, filename,
retry_after and error (the JSON error reply otherwise). GIF exports need a
registered account, so any personal token or device session works, and they
share the site limit on GIF exports: honor retry_after after a 429. This saves
a GIF of a public archive game:
import os from classicchess_api import ApplicationClient gif = ApplicationClient().public_game_gif("tal-vs-larsen-1965", os.environ["CLASSICCHESS_API_TOKEN"]) if gif.ok: with open(gif.filename or "game.gif", "wb") as handle: handle.write(gif.content) else: print(gif.status, gif.error)
Any account can play the Gym's bots; the moves are computed on the server.
Personal tokens need gym:read to read games and gym:play to start and play
them. Send the ply from the latest game state with each move: a stale ply
returns 409 and plays nothing, so a repeated request is safe. The bot engine
plays one move at a time. While it is busy, account_gym_bot_move returns 429
with capacity_exhausted: wait retry_after seconds and ask again with the
same ply. Each account may use 30 minutes of engine time in each hour; once it
is spent the answer is 429 engine_budget with retry_after set to the seconds
until that hour ends. An account starts at most 30 games an hour.
import os import time from classicchess_api import ApplicationClient token = os.environ["CLASSICCHESS_API_TOKEN"] # a token with gym:read and gym:play api = ApplicationClient() created = api.account_gym_new_game("morphy", token, color="white", time="5+0") if not created.ok: raise SystemExit(f"{created.status} {created.data}") game = api.account_gym_move(created.data["id"], "e2e4", created.data["ply"], token).data reply = api.account_gym_bot_move(game["id"], game["ply"], token) while reply.status == 429 and reply.data["error"]["code"] == "capacity_exhausted": time.sleep(int(reply.retry_after or 1)) reply = api.account_gym_bot_move(game["id"], game["ply"], token) print(reply.status, reply.data.get("sans"), reply.data.get("clock"))
Puzzles come from master games and are for signed-in accounts only. Personal
tokens need puzzles:read to read puzzles, figures and history and
puzzles:write to save a result or reset; cookie sessions are never enough.
Each account has its own rotation: a puzzle solved on the first try leaves it
until reset, and a missed one stays. A puzzle carries the game so far
(start_fen and lead), the position (fen) and solution.moves: the
accepted solver moves at each point are the entries whose after equals the
moves played so far. Save each play once, missed at the first wrong move or
when the solution is shown and solved for a clean finish. A 404
rotation_empty reply means every puzzle is solved; reset to play them again.
This plays one puzzle in the terminal and saves how it went:
import os from classicchess_api import ApplicationClient token = os.environ["CLASSICCHESS_API_TOKEN"] # a token with puzzles:read and puzzles:write api = ApplicationClient() puzzle = api.account_puzzle_next(token) if not puzzle.ok: raise SystemExit(f"{puzzle.status} {puzzle.data}") print(puzzle.data["prompt"], puzzle.data["fen"]) played, result = [], "solved" while True: accepted = {m["move"]: m["reply"] for m in puzzle.data["solution"]["moves"] if m["after"] == played} answer = input("Your move in UCI, such as e2e4: ").strip() if answer not in accepted: result = "missed" break played.append(answer) if accepted[answer] is None: break print("Reply:", accepted[answer]) played.append(accepted[answer]) saved = api.account_puzzle_result(puzzle.data["id"], result, token) print(saved.data["result"], saved.data["summary"])
The CLI reads CLASSICCHESS_API_TOKEN for account commands. Existing installations
can continue using their previous token environment variable as a fallback.
The original account helpers on ClassicChessClient remain available and raise
ApiError for unsuccessful HTTP responses.
Configure ClassicChessClient(timeout=30, max_response_bytes=32 * 1024 * 1024)
or ApplicationClient with the same options. Public reads return dictionaries
or text; ApiError exposes status_code, code and retry_after when available.
Pagination rejects repeated pages and links outside the original API collection.
The application transport sends no cookies, follows no redirects, and accepts
bearers only on private API paths over HTTPS or loopback development.
There are no automatic retries. Respect Retry-After and avoid blindly retrying writes.
These helpers require a personal token with library:read for
lookup and library:write for creation, deletion and bulk additions.
ApplicationClient.account_create_collection(name, token) creates an empty
collection. account_find_collection(name, token) returns count and results
for the exact name ignoring case (empty when absent). Use the returned ID.
account_delete_collection(id, token) deletes that collection, including its
memberships, notes and progress, while retaining the underlying games.
account_add_curated_games(id, filters, token, ...) processes one batch, never
MasterDB or user imports. Each filter group's criteria are AND; groups are OR;
exclude subtracts groups. Filter keys are players, events, since, until,
notable, q, theme, eco, result, annotation_q, annotation_author, and
annotation_book. Players/events take lists of catalog slugs, OR within the
list; players match either participant, including event games. Use
ClassicChessClient.public_players(...) and public_events(...) to find slugs.
Years are inclusive, including games with unknown months or days.
[{"notable": True}] selects notable games across every curated player.
For every curated game played in 1920, save this as collections_example.py.
Set CLASSICCHESS_API_TOKEN in your private environment, then run
python collections_example.py. It creates a collection and adds all batches:
import os from classicchess_api import ApplicationClient api = ApplicationClient() token = os.environ["CLASSICCHESS_API_TOKEN"] created = api.account_create_collection("Curated games from 1920", token) if not created.ok: raise RuntimeError(created.data) collection_id = created.data["id"] filters = [{"since": 1920, "until": 1920}] preview = api.account_add_curated_games(collection_id, filters, token, dry_run=True) if not preview.ok: raise RuntimeError(preview.data) print("Matched:", preview.data["matched"]) after = 0 while True: response = api.account_add_curated_games(collection_id, filters, token, after_game_id=after) if not response.ok: raise RuntimeError(response.data) print("Added this batch:", response.data["added"]) if not response.data["has_more"]: break after = response.data["next_after_game_id"]
Keep filters/exclusions fixed while paging. batch_size is 1–1000 (default
1000); after_game_id starts at 0. matched counts the entire live selection;
added, already_present and processed count this batch. A dry run writes
nothing; begin actual additions at 0 afterward. Retrying a batch preserves
existing entries, notes and progress. The catalog is reevaluated on each call;
there is no frozen snapshot or automatic write retry. Stop on errors and retain
the last successfully returned cursor if you need to resume.
The same three helpers exist on ClassicChessClient, with api_token= instead
of positional token, and its usual exception-on-HTTP-error behavior. The
application client instead retains HTTP errors in status/data.
The standalone CLI retains its existing commands; these are library helpers.
If you only want command-line commands without installing the Python package,
GitHub also provides pull_api.py. Save that file and run
python3 pull_api.py public players --names. It is the dependency-free CLI
variant; the importable SDK and application transport are installed by the
steps above. The Classic Chess website no longer serves a script download URI.
These contributor commands start from a fresh clone:
git clone https://github.com/Andromeda1957/classicchess-api-python.git cd classicchess-api-python python3 -m venv .venv source .venv/bin/activate python3 -m unittest discover -s classicchess_api/tests -t . python3 -m pip install .
The same API capabilities are available in the TypeScript SDK and Kotlin SDK. See the API reference for response fields, endpoint permissions, rate limits and data attribution. The MIT license covers the SDK code; linked chess resources retain their own stated reuse terms. Issues and pull requests are welcome; see CONTRIBUTING.md.
These helpers require the matching server release. Search comments, glyphs and diagrams with
account_notebook_annotation_search(token, q="king", kind="comments", notebook_uuid=None, limit=50, cursor=None).
It searches owned and directly invited Notebooks. Open a hit with its authorized PGN URL and
child-index path; keep its content_version to detect edits. Follow a non-null cursor,
including after an empty page. complete=False reports a bounded or partial scan.
account_puzzle_next(token, result="missed") and account_puzzle_history(token, result="missed")
review current incorrect attempts. account_puzzle_batch(token, limit=25) downloads 1–100
playable puzzles. Keep the returned account ID, generation and each puzzle revision in
account-separated private storage. Save each attempt locally before sending
account_puzzle_result(id, result, token, event_id=uuid, generation=generation, revision=revision);
retry the same UUID and fields until acknowledged. Saved/duplicate responses acknowledge it;
409 stale_generation/stale_puzzle/event_conflict and 404 are terminal. Keep queued attempts
at their original reset generation. The SDK provides transport; your app owns durable storage,
network retry/backoff, account isolation and sign-out cleanup.