PokerKit, hands-on

What the library is, the five rules that trip people up, and seven labs you run on your own laptop Back to index
LanguagePure Python 3.11+
Installpip install pokerkit
Variants built in15
Source~15k lines
LicenseMIT
FromU of T CPRG
If you remember one thing

PokerKit is the dealer, not the player.

It knows every rule of poker: whose turn it is, what raise is legal, who wins the pot, where the chips go. It never decides what to do. That part is you, or the bot you write. Everything on this page is a consequence of that one sentence.

Before you start

Read this page three times, differently each time

Read 1 · 5 min
Skim the shape

Read only the headings, the four jobs, and the five rules. Skip every code block. You are building the map.

Read 2 · 45 min
Run the labs

Type each lab yourself, do not paste. Answer the "Predict first" box before you run it. Being wrong once is what makes it stick.

Read 3 · next day
Close it and recall

Jump to the recall cards at the bottom. Answer out loud before you open each one. Anything you miss, redo that lab.

Section 1 · The map

One library, four jobs

Picture a real card room. PokerKit ships four people you would find there. Every class you will ever import belongs to one of them.

Dealer
Runs one hand from blinds to payout and refuses illegal moves.
state.py · games.py
State, NoLimitTexasHoldem
Judge
Looks at cards and says which hand is stronger.
hands.py · lookups.py
StandardHighHand
Calculator
Turns ranges into combos and estimates equity by simulation.
analysis.py
parse_range, calculate_equities
Scribe
Writes hands down as text and replays them later.
notation.py
HandHistory (.phh)

Line counts from the repo: state.py 6,439 · notation.py 2,720 · games.py 2,442 · hands.py 851 · analysis.py 555. The Dealer is almost half the library, because poker rules have many corner cases (side pots, all-in runouts, stud bring-ins, draw games).

Section 2 · How you talk to the Dealer

Every program is the same four-step loop

1. Creategame(stacks, players) gives you a fresh state: one hand of poker.
2. Askstate.actor_index says whose turn. state.can_fold() etc. say what is legal.
3. Actfold(), check_or_call(), complete_bet_or_raise_to(x), deal_hole(...)
4. Readstate.stacks, state.board_cards, and at the end state.payoffs.

Repeat steps 2 and 3 while state.status:. When status turns False, the hand is over.

What "Automation" means

A hand has many small chores. Each blue pill below is something you must always do. Each grey pill is a chore you can hand to the Dealer by listing it in automations.

post antespost blindsburn carddeal holedeal board fold · check/call · bet/raisediscard (draw games) collect betsshow / muckkill losing handspush chipspull chips

Poker bot? Automate every grey pill. Want to choose the cards (to test one specific spot)? Leave HOLE_DEALING and BOARD_DEALING out, then call deal_hole('AsKs') yourself. Building a real online table? Automate nothing and animate every step.

Section 3 · The rules that trip people up

Five things to remember

Raise TO, never raise BY.

complete_bet_or_raise_to(6) means "my total bet this street becomes 6", not "add 6". One method covers complete, bet and raise.

Index = position, not seat. Heads-up flips it.

With 3+ players: 0 = small blind, 1 = big blind, last = button. With 2 players: 0 = big blind, 1 = button/small blind, so player 1 acts first preflop. Lab 2 shows this.

Every action comes in threes.

verify_folding() raises an error, can_fold() returns True/False, fold() does it. An illegal action raises ValueError and leaves the state untouched.

Cards are plain strings.

'AsKs' is two cards. Ranks 23456789TJQKA, suits cdhs. '??' is an unknown card, handy for hands where you never saw the cards.

Python counts from 0, hand histories from 1.

In code, the first player is 0. In a saved .phh file the same player is p1. Also, board_cards is a list of lists (one list per card slot) because PokerKit supports multiple boards and run-it-twice.

Section 4 · Labs

LAB 0

Set up a sandbox

3 min

PokerKit needs Python 3.11 or newer and has no other dependencies. Use a virtual environment so nothing touches your system Python.

terminal
python3 --version            # must say 3.11 or higher
mkdir pokerkit-lab && cd pokerkit-lab
python3 -m venv .venv
source .venv/bin/activate    # Windows: .venv\Scripts\activate
pip install pokerkit
python -c "import pokerkit; print('ready')"

Every lab below is one file. Save it inside pokerkit-lab/ and run python lab1.py. All outputs shown were produced by actually running the code.

LAB 1

Meet the Judge

5 min

Start with the smallest piece. from_game(hole, board) finds the best 5-card hand, and hands compare like numbers.

lab1.py
from pokerkit import StandardHighHand

mine  = StandardHighHand.from_game('AsKs', 'QsJsTs2c3d')
yours = StandardHighHand.from_game('AhAd', 'QsJsTs2c3d')

print(mine)          # the best 5 cards it found
print(yours)
print(mine > yours)  # compare like numbers
Predict firstWhat hand does mine make, and what does yours make? Write both down.
Check yourself

A royal (reported as straight flush) against one pair of aces. The Judge picked the best 5 of 7 cards on its own.

Straight flush (AsKsQsJsTs)
One pair (AhAdQsJsTs)
True
Try it: compare StandardHighHand('7c5d4h3s2c') > StandardHighHand('AcAd9h8s2c'), then the same two hands as StandardLowHand (2-7 lowball). The first is False, the second True. In PokerKit "greater" always means "wins", even in low games.
LAB 2

Play one hand with the Dealer

10 min

Heads-up No-Limit Hold'em, blinds 1/2, 200 chips each. We leave dealing out of automations so we pick the cards.

lab2.py
from pokerkit import Automation, NoLimitTexasHoldem

state = NoLimitTexasHoldem.create_state(
    (
        Automation.ANTE_POSTING,
        Automation.BET_COLLECTION,
        Automation.BLIND_OR_STRADDLE_POSTING,
        Automation.CARD_BURNING,
        Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
        Automation.HAND_KILLING,
        Automation.CHIPS_PUSHING,
        Automation.CHIPS_PULLING,
    ),
    True,     # uniform antes?
    0,        # antes
    (1, 2),   # blinds
    2,        # min-bet
    200,      # every stack = 200
    2,        # players
)

state.deal_hole('AsKs')   # player 0
state.deal_hole('7h7d')   # player 1
print('stacks  :', state.stacks)
print('to act  :', state.actor_index)

state.complete_bet_or_raise_to(6)   # player 1 raises TO 6
state.check_or_call()               # player 0 calls
state.deal_board('Qs Js 2c')        # flop

state.check_or_call()               # player 0 checks
state.complete_bet_or_raise_to(8)   # player 1 bets 8
state.fold()                        # player 0 folds

print('running?:', state.status)
print('stacks  :', state.stacks)
print('payoffs :', state.payoffs)
Predict firstBefore anyone acts, the first print shows the stacks. Who paid 2 and who paid 1? And who acts first?
Check yourself

[198, 199] and to act: 1. Heads-up, player 0 is the big blind and player 1 is the button, who posts the small blind and acts first preflop. That is rule 2.

stacks  : [198, 199]
to act  : 1
running?: False
stacks  : [194, 206]
payoffs : [-6, 6]

Notice the payoff is 6, not 14. Player 1's flop bet of 8 was never called, so the Dealer handed it back. You wrote zero lines of code for that.

Try it: replace state.fold() with state.check_or_call(). Now status stays True. Print state.street_index, deal a turn with state.deal_board('9d'), and keep playing to showdown.
LAB 3

Try to break the rules

5 min

Good referees say no. Ask before acting, then act illegally on purpose.

lab3.py
from pokerkit import Automation, NoLimitTexasHoldem

state = NoLimitTexasHoldem.create_state(
    (Automation.ANTE_POSTING, Automation.BET_COLLECTION,
     Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING,
     Automation.HOLE_DEALING, Automation.BOARD_DEALING,
     Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING,
     Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING),
    True, 0, (1, 2), 2, 200, 2,
)

print(state.can_complete_bet_or_raise_to(3))
print(state.min_completion_betting_or_raising_to_amount)
try:
    state.complete_bet_or_raise_to(3)
except ValueError as e:
    print('ValueError:', e)
print(state.stacks)
Predict firstBig blind is 2. What is the smallest legal raise-to? Do the stacks change after the failed raise?
Check yourself

Minimum raise-to is 4 (call 2, raise by at least 2). The failed raise leaves stacks exactly as before. This time the Dealer dealt random hole cards, because HOLE_DEALING is automated.

False
4
ValueError: The amount 3 is below the minimum allowed 4.
[198, 199]
Try it: print state.max_completion_betting_or_raising_to_amount. In no-limit it is your whole stack. Change the game to PotLimitOmahaHoldem (deal 4 hole cards) and watch the max shrink to the pot.
LAB 4

Use the Calculator

5 min

Take the flop from Lab 2. Should player 0 really have folded AsKs there?

lab4.py
from pokerkit import (
    Card, Deck, StandardHighHand, calculate_equities, parse_range,
)

print(len(parse_range('AKs')), 'combos of AKs')
print(len(parse_range('AK')),  'combos of AK')
print(len(parse_range('22')),  'combos of 22')

eq = calculate_equities(
    (parse_range('AsKs'), parse_range('7h7d')),  # the two hands
    Card.parse('QsJs2c'),                        # board so far
    2,                    # hole cards per player
    5,                    # board cards at the end
    Deck.STANDARD,
    (StandardHighHand,),
    sample_count=20000,
)
print('AsKs vs 7h7d on QsJs2c:', eq)
Predict firstAsKs has two overcards, a flush draw and a gutshot. Guess its equity against 77 before you run it.
Check yourself

About 63%. The fold in Lab 2 threw away a favourite. The Dealer allowed it because the Dealer never judges decisions, only legality.

4 combos of AKs
16 combos of AK
6 combos of 22
AsKs vs 7h7d on QsJs2c: [0.6321, 0.3679]

It is a Monte Carlo estimate, so your last digits will differ. 20,000 samples took about 5 seconds on one core. Pass executor=ProcessPoolExecutor() to use every core.

Try it: ranges are Python sets. parse_range('AK') - parse_range('AKs') gives the 12 offsuit combos. Run AK against parse_range('22-99') preflop (board ()).
LAB 5

1,000 hands of random bots

10 min

This is where PokerKit earns its place. Automate every chore, write a bot that picks any legal action, and let three of them play. The while state.status loop is the whole engine.

lab5.py
import random
from pokerkit import Automation, NoLimitTexasHoldem

AUTO = (
    Automation.ANTE_POSTING, Automation.BET_COLLECTION,
    Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING,
    Automation.HOLE_DEALING, Automation.BOARD_DEALING,
    Automation.RUNOUT_COUNT_SELECTION,
    Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING,
    Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING,
)
game = NoLimitTexasHoldem(AUTO, True, 0, (1, 2), 2)   # a factory

def random_bot(state):
    choices = []
    if state.can_fold():
        choices.append('fold')
    if state.can_check_or_call():
        choices.append('call')
    if state.can_complete_bet_or_raise_to():
        choices.append('raise')
    pick = random.choice(choices)
    if pick == 'fold':
        state.fold()
    elif pick == 'call':
        state.check_or_call()
    else:
        lo = state.min_completion_betting_or_raising_to_amount
        hi = state.max_completion_betting_or_raising_to_amount
        state.complete_bet_or_raise_to(random.randint(lo, hi))

random.seed(1)
totals = [0, 0, 0]
for _ in range(1000):
    state = game(100, 3)            # fresh hand: 3 players, 100 chips
    while state.status:
        random_bot(state)
    assert sum(state.payoffs) == 0
    totals = [t + p for t, p in zip(totals, state.payoffs)]

print('net chips after 1000 hands:', totals)
print('sum:', sum(totals))
Predict firstCould the assert ever fail? What must sum be?
Check yourself

Poker is zero-sum with no rake, so the sum is always 0. If it ever failed you would have found a bug in the Dealer. This check is the cheapest test you can put in any bot project.

net chips after 1000 hands: [692, 3739, -4431]
sum: 0

1,000 full hands ran in under a second. Notice game is created once and called like a function for each new hand: game = the rules, state = one hand.

Try it: write calling_station(state) that always calls when it can, give it seat 0, and keep the random bots in seats 1 and 2. Who wins over 5,000 hands? This is your first bot experiment.
LAB 6

Kuhn poker, the game from this site's CFR pages

5 min

PokerKit ships KuhnPoker: three cards (J, Q, K), ante 1, one bet of 1. It is the same toy game used in the CFR simulation, so you can use PokerKit as the rules engine for your own CFR.

lab6.py
from pokerkit import Automation, KuhnPoker

state = KuhnPoker.create_state(
    (
        Automation.ANTE_POSTING, Automation.BET_COLLECTION,
        Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING,
        Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING,
        Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING,
    ),
)
state.deal_hole('Js')     # player 0 gets the Jack
state.deal_hole('Ks')     # player 1 gets the King

state.complete_bet_or_raise_to()   # P0 bluffs with J
state.check_or_call()              # P1 calls with K

print('payoffs:', state.payoffs)
Predict firstHow many chips does the bluff lose?
Check yourself

2: the ante of 1 plus the bet of 1. In fixed-limit games complete_bet_or_raise_to() needs no amount, the size is fixed.

payoffs: [-2, 2]
Try it: loop over all 6 deals of (J, Q, K) and every action sequence, and print the payoff table. That table is exactly what vanilla CFR walks each iteration.
LAB 7

Let the Scribe write it down

5 min

Replay Lab 2, save it as a .phh file (Poker Hand History, a plain-text format), then load it back and step through it.

lab7.py
from pokerkit import Automation, HandHistory, NoLimitTexasHoldem

game = NoLimitTexasHoldem(
    (Automation.ANTE_POSTING, Automation.BET_COLLECTION,
     Automation.BLIND_OR_STRADDLE_POSTING, Automation.CARD_BURNING,
     Automation.HOLE_CARDS_SHOWING_OR_MUCKING, Automation.HAND_KILLING,
     Automation.CHIPS_PUSHING, Automation.CHIPS_PULLING),
    True, 0, (1, 2), 2,
)
state = game(200, 2)
state.deal_hole('AsKs')
state.deal_hole('7h7d')
state.complete_bet_or_raise_to(6)
state.check_or_call()
state.deal_board('QsJs2c')
state.check_or_call()
state.complete_bet_or_raise_to(8)
state.fold()

hh = HandHistory.from_game_state(game, state)
hh.players = ['Nut', 'Bot']
with open('my_hand.phh', 'wb') as f:
    hh.dump(f)
print(open('my_hand.phh').read())

with open('my_hand.phh', 'rb') as f:
    replay = HandHistory.load(f)
for i, s in enumerate(replay):   # one state per action
    print(i, s.stacks)
variant = 'NT'
ante_trimming_status = false
antes = [0, 0]
blinds_or_straddles = [1, 2]
min_bet = 2
starting_stacks = [200, 200]
actions = ['d dh p1 AsKs', 'd dh p2 7h7d', 'p2 cbr 6', 'p1 cc', 'd db QsJs2c', 'p1 cc', 'p2 cbr 8', 'p1 f']
players = ['Nut', 'Bot']

0 [198, 199]
1 [198, 199]
2 [198, 199]
3 [198, 194]
4 [194, 194]
5 [194, 194]
6 [194, 194]
7 [194, 186]
8 [194, 206]

Read the action codes once and you can read any .phh file

CodeMeaningPython call
d dh p1 AsKsdealer deals hole cards to player 1deal_hole('AsKs')
d db QsJs2cdealer deals boarddeal_board('QsJs2c')
p2 cbr 6player 2 completes / bets / raises to 6complete_bet_or_raise_to(6)
p1 ccplayer 1 checks or callscheck_or_call()
p1 fplayer 1 foldsfold()
p1 sdstand pat / discard (draw games)stand_pat_or_discard()

Note p1 in the file is player 0 in Python (rule 5). The Scribe can also import PokerStars, Full Tilt, PartyPoker, iPoker, OnGame, Absolute and ACPC logs with HandHistory.from_pokerstars(...) and friends.

Section 5 · Where it fits

What PokerKit is for, and what it is not

Use it for

  • The environment a poker bot plays inside
  • Checking your own engine's rules against a trusted one
  • Parsing and replaying hand histories
  • Quick equity checks and range math
  • Odd variants: 2-7 triple draw, badugi, stud, short deck, PLO

It is not

  • A solver. It computes no strategy, no GTO, no EV of a decision
  • Fast enough for billions of hands. Pure Python by design
  • A UI. It is a library; you draw the table yourself

Speed, from the project's own benchmark (one core, i7-1255U)

TaskPokerKitCompare
Full hands simulated847 / sfine for testing and replay, slow for CFR on Hold'em
5-card hand evaluations1,016,741 / streys: 3,230,966 / s

Rule of thumb: prototype with PokerKit, and when a solver needs raw speed, keep PokerKit as the referee that checks your fast engine gives the same payoffs.

Section 6 · Read 3

Recall cards: answer out loud, then open

In one sentence, what is PokerKit?

The dealer, not the player. It enforces the rules and moves the chips; it never chooses actions.

Name the four jobs and one class for each.

Dealer (NoLimitTexasHoldem / State), Judge (StandardHighHand), Calculator (calculate_equities), Scribe (HandHistory).

What four steps does every PokerKit program repeat?

Create a state, ask (actor_index, can_*), act, read (stacks, payoffs). Loop while state.status.

Blinds 1/2. Player raises to 6. What do you call?

complete_bet_or_raise_to(6). The amount is the total, not the increment.

Heads-up: who is player 0, and who acts first preflop?

Player 0 is the big blind. Player 1 is the button/small blind and acts first preflop.

What happens when you make an illegal move?

ValueError, and the state does not change. Use can_* to check first.

You want to choose the hole cards yourself. What do you change?

Remove Automation.HOLE_DEALING (and BOARD_DEALING) from the automations, then call deal_hole('AsKs').

Which one-line test catches most engine bugs?

assert sum(state.payoffs) == 0 after every hand (no rake).

What is the difference between game and state?

game holds the rules and blinds; calling game(stacks, players) makes a new state, which is one hand.

In a .phh file, what does p2 cbr 8 mean?

Player 2 (Python index 1) completes, bets or raises to 8.

Sources

Where to go deeper

github.com/uoftcprg/pokerkit · source, README with famous televised hands replayed in code
pokerkit.readthedocs.io · simulation, evaluation, analysis and notation guides
phh.readthedocs.io · the Poker Hand History file format spec
Kim, J. PokerKit (IEEE Transactions on Games, 2025) · the paper, if you ever need to cite it