This tool is for playing with Portable Game Notation (PGN) files, by allowing to split them up into several chunks and also shuffling them. The reason for chunking is that Lichess silently drops everything past game 64 when you import a PGN into a study. Shuffling becomes useful for training, as if your PGN file is sorted by opening or theme, the game number already hints at what position/tactic/whatever is coming. Shuffle it first and you have to actually recognise the position rather than remember where it is in the file.
Python 3.10+, no dependencies.
git clone https://github.com/your-username/pgn-prep
cd pgn-prep
python pgn_prep.py my_games.pgnOr pip install -e . if you want a pgn-prep command on your PATH.
# split into 64-game chunks
python pgn_prep.py tournament_2024.pgn
# shuffle then split into 32-game chunks
python pgn_prep.py repertoire.pgn --shuffle --chunk-size 32
# same seed = same shuffle every time, useful for sharing with others
python pgn_prep.py repertoire.pgn --seed 42
# named chapters — won't cut across opening sections
python pgn_prep.py repertoire.pgn --split-at 25,55 --names "1.e4,1.d4,Flank"
# see what would happen without writing anything
python pgn_prep.py repertoire.pgn --dry-run --verboseIf your PGN is divided into sections (openings, endgame themes, whatever), --split-at lets you mark where each section ends so the chunks never cross those boundaries:
python pgn_prep.py repertoire.pgn \
--split-at 30,75,110 \
--names "Open Games,Semi-Open,Closed,Flank"This gives you Open Games.pgn, Semi-Open.pgn, and so on. If a section still has more than 64 games it gets split further and the pieces are numbered (Open Games_1.pgn, Open Games_2.pgn).
--names works without --split-at too — handy if you just want the output called something other than repertoire_part1.pgn.
| default | ||
|---|---|---|
--chunk-size N |
64 | max games per file |
--shuffle |
off | randomise order before splitting |
--seed N |
— | pin the shuffle; implies --shuffle |
--split-at N1,N2,... |
— | force splits at these game numbers |
--names NAME,... |
— | filenames for each --split-at segment |
--output-dir DIR |
next to source | where to write files |
--prefix STR |
— | prepend to every output filename |
--dry-run |
off | print plan, don't write |
--encoding ENC |
utf-8-sig | strips BOM automatically |
--no-number-pad |
off | _part1 not _part01 |
--verbose |
off | print each game as it's processed |
--remove-nulls |
off | strip null moves (--) so it can be imported to Lichess with no errors |
The script finds game boundaries in the raw text rather than parsing moves. A line matching [TagName "Value"] that isn't inside a { comment } or ( variation ) marks the start of a new game header. Brace and parenthesis depth are tracked as it scans, so annotation text that happens to look like a tag pair doesn't trigger a false split. Everything between boundaries — moves, comments, NAGs, variations — passes through untouched.
Output files are always UTF-8, no BOM. Games are separated by one blank line regardless of how the original was formatted.
This won't fix a broken PGN. If Lichess rejects a chunk after import, the problem was in the original file, not something introduced here.
MIT — Mikel Iturbe