Skip to main content

Guide to the Parser

CPython uses a PEG (Parsing Expression Grammar) parser introduced in Python 3.9 to replace the original LL(1) parser.

PEG Parser Overview

The parser is generated from a grammar file by a parser generator, not hand-written.

Key Files

  • Grammar/python.gram - Python grammar specification
  • Parser/parser.c - Generated C parser code
  • Tools/peg_generator/ - Parser generator (Pegen)
To change Python’s syntax, modify Grammar/python.gram, then run make regen-pegen.

How PEG Parsers Work

PEG differs fundamentally from context-free grammars.

Ordered Choice

The choice operator (|) is ordered:
  • PEG: Try A first. If succeeds, done. Otherwise try B, then C.
  • CFG: Deduce which alternative applies based on lookahead.
Critical Difference: In PEG, A | B is NOT the same as B | A!

Example: Order Matters

  • first_rule accepts aa but not aaa
    • 'a' matches first char, leaving 'aa' for the final 'a' (fails)
  • second_rule accepts aaa but not aa
    • 'aa' matches first two chars, leaving one for final 'a'

Eagerness

PEG parsers are eager: Once an alternative succeeds, no backtracking to try other alternatives even if the overall parse fails.

Common Mistake

This grammar is wrong:
Second alternative never tried! First alternative always matches and consumes the input, leaving no chance for second. Fix: Reorder alternatives (longest first):

Grammar Syntax

Basic Rule Format

With return type:

Grammar Expressions

Variables

Name sub-expressions for use in actions:

Actions

Specify return value in curly braces:
Critical: Actions must NOT mutate AST nodes passed via variables. Nodes are cached by memoization and might be reused. Create new copies if modification needed.

Left Recursion

PEG parsers typically don’t support left recursion, but CPython’s does!

Examples

Simple:
Indirect:
Hidden:
All work in CPython’s parser!

Memoization

Memoization caches parse results to avoid exponential time.

Selective Memoization

CPython disables memoization by default except for rules marked (memo):
Left-recursive rules always use memoization (required for the algorithm).

Why Selective?

Full memoization:
  • Uses significant memory
  • Has execution overhead (cache lookups)
  • Often slower than re-parsing for simple rules
Benchmark each rule to determine if memoization helps.

Keywords

Hard Keywords

Always reserved (single quotes in grammar):
Cannot be used as identifiers:

Soft Keywords

Contextual (double quotes in grammar):
Only keywords in specific contexts:

Listing Keywords

Error Handling

Generic Errors

PEG’s error heuristic: Report error at furthest token that failed to match.
This heuristic works well for Python’s grammar but isn’t perfect. Lookaheads can affect error location.

Custom Errors

Use invalid_ rules for better error messages:

Two-Phase Parsing

Phase 1: Parse without invalid_ rules
  • If succeeds: Return AST, skip phase 2
Phase 2: Parse with invalid_ rules (only if phase 1 failed)
  • Try to match specific error patterns
  • Raise custom SyntaxError with helpful message
All invalid_ rules must start with invalid_ prefix to avoid impacting phase 1 performance.

Testing Invalid Rules

Add syntax error after valid code:
If your invalid_ rule works, error reports at $. If not, it reports elsewhere.

Automatic Variables

Pegen injects variables into action scope:
  • p - Parser structure
  • EXTRA - Expands to (_start_lineno, _start_col_offset, _end_lineno, _end_col_offset, p->arena)
Used in AST node constructors:

Regenerating the Parser

After Grammar Changes

Windows:

Regenerating Meta-Parser

If you modify Tools/peg_generator/pegen/metagrammar.gram:

Tokenization

CPython’s tokenizer is separate from the parser (unlike many PEG implementations).

Token List

Defined in Grammar/Tokens. After modifying:

Why Separate Tokenizer?

Python needs custom tokenization for:
  • Indentation handling (INDENT/DEDENT tokens)
  • Soft keywords (async, await compatibility)
  • Interactive mode
  • Encoding detection
  • Better error messages

Debugging

Python Parser (Experimentation)

Generate Python parser for testing:
Test it:
Python parser easier to debug than C version.

Verbose Mode

Compile Python in debug mode:
Run with verbose parsing:
Output format:
  • > - Trying to parse rule
  • + - Rule parsed successfully
  • - - Rule failed
  • ! - Exception/error detected

Example Grammar

Simple arithmetic parser:
This grammar:
  • Parses expressions with +, -, *, /
  • Handles parentheses
  • Respects operator precedence (multiplication before addition)
  • Uses left recursion for left associativity

Testing

Grammar tests in: Parser generator tests: