Programmer writing source code for a tiny interpreter

Python Interpreter Development Guide

September 7, 2026 · 8 min read · By Thomas A. Anderson

Key Takeaways:

  • Henley’s 1024-byte Python interpreter runs Python-looking code (FizzBuzz, recursion, operator precedence) with no separate lexer, AST, or bytecode stage; all runtime state is five globals.
  • The architectural trick is a recursive descent parser that evaluates as it parses, so the C call stack doubles as the expression tree and operator precedence results from function nesting.
  • Removing error handling, multi-character identifiers, and function arguments is what makes the byte budget work, and those same removals reveal exactly where production language engineering happens.
  • Code golf itself is a weak habit; the lasting lesson is that a fixed size cap forces a real design decision (single-pass execution).

The Target Program

Austin Henley set the budget before writing any code. In a September 2026 post, he describes it as a weekend challenge: build “a Python interpreter in 1024 bytes of good ole C” under two self-imposed rules, no macro tricks and no library calls beyond libc. He began at 512 bytes, built what he calls “a calculator,” and stopped when he ran over the limit. Doubling to 1024 bytes, he anchored the whole project to a single target program, this FizzBuzz:

Executing While You Parse
def buzz():
 for n in range(101):
 if n % 15 == 0:
 print("FizzBuzz")
 else:
 if n % 3 == 0:
 print("Fizz")
 else:
 if n % 5 == 0:
 print("Buzz")
 else:
 print(n)
buzz()

That is not a toy calculator. It uses def, indentation-scoped blocks, a for ... in range() loop, modulo arithmetic, nested if/else, and string output. Henley’s reasoning is that this program “looks distinctly Python”: it has colons, indentation, and no parentheses around the if condition. The finished interpreter handles all of it, plus a feature list that reads like a stripped-down language specification: integer variables and literals, assignment, arithmetic with + - * % and correct precedence, comparisons with = and ==, integer truthiness, if/else, while loops with else, for x in range(y) loops with else, function definitions with no arguments, recursive function calls, indentation-based blocks without scope, print of a string literal or an integer expression, and comments.

None of this matches what real CPython does. As Henley notes, CPython tokenizes source, parses it into an abstract syntax tree, performs analysis and optimizations, emits bytecode, and then interprets that bytecode. His version skips every one of those stages. The difference between the two is the article’s real subject, because the machinery CPython carries exists to handle scale, error messages, and optimization, none of which a 1024-byte interpreter can afford.

The State: Five Globals

All the runtime state is stored in five globals. The raw source text lives in a fixed 999-byte array, variables and function names share a 256-entry integer table, and three cursors track the current character and where the line started.

char src[999]; /* Entire program without most spaces. */
int vars[256]; /* Symbol table. */
int pos; /* Index of next character in src. */
int ch; /* Current character in src. */
int line_start;/* Where the current line starts. */

The symbol table is the smallest deliberate trick, and it works only because of a hard restriction. Variable and function names are limited to single lowercase letters, so a lookup becomes a direct array index: read the character, then read vars[ch]. No hashing, no string comparison, no dictionary. The parser prefers a variable name and then falls back to assembling an integer literal from digits.

int parse_atom(void) {
 int value = 0;
 next();
 if (ch > 96) { /* lowercase letter? */
 value = vars[ch]; /* direct symbol table lookup */
 next();
 }
 while (ch >= '0' && ch <= '9') {
 value = value * 10 + ch - '0';
 next();
 }
 return value;
}

The check ch > 96 tests whether the current character is a lowercase ASCII letter, and if so, vars[ch] reads the variable's value straight out of the table. This works because the language subset was chosen to allow it: one-letter names, no types, no scoping. Limiting identifiers to one character turns the table into an array rather than a hash map. Real interpreters pay for dictionaries, scoping, and multi-character names; this one avoids those by refusing to support them.

There is no error handling anywhere. The parser assumes keywords are spelled correctly, token boundaries are right, and indentation is consistent. As Henley puts it, "it makes a lot of assumptions based on correctness of code." Breaking one of those assumptions causes the program to misbehave rather than report a syntax error. For contrast, real CPython on constrained hardware is a different problem entirely, covered in our CPython RISC-V analysis.

Executing While You Parse

The decision that makes everything fit is running code during the parse rather than building a tree and walking it later. Precedence results from function nesting: parse_sum calls parse_term, which calls parse_atom. Operator binding decides which level runs next, so the C call stack itself is the expression tree. There are no heap-allocated nodes and no intermediate representation. Where a textbook interpreter allocates token structs, builds an AST, and runs a tree walker or a bytecode dispatch loop, this one has none of that: no token array, no AST nodes, no allocation, no separate pass boundaries to keep in sync.

Control flow works the same way. A block runs until indentation drops, then the function returns to its caller. while loops, for loops, and recursive functions use the same C recursion, with a function body entered by jumping to a saved source position. The readable source's run_block function reads indentation and dispatches on the first character of each statement, then returns when the indentation decreases.

The keyword dispatch shows how far the constraint pushes even the readable code. The parser checks whether the current character is w, i, or f, then figures out which keyword it is looking at by skipping a fixed number of characters:

if (ch == 'w' || ch == 'i' || ch == 'f') {
 int keyword = ch;
 int loop_var = 0;
 if (keyword == 'f') { /* "for k in range(n):" (or "def") */
 pos += 2; /* skip "or" of "for" */
 loop_var = next();
 pos += 8; /* skip "inrange(" */
 vars[loop_var] = 0;
 } else if (keyword == 'w') {
 pos += 4; /* skip "hile" of "while" */
 } else {
 pos += 1; /* skip "f" of "if" */
 }

pos += 4 skips "hile" of "while", pos += 1 skips the "f" of "if", and pos += 8 skips "inrange(" of a for loop. This is fragile by design: it assumes keywords are typed out fully and correctly. It results from not spending any bytes on a lexer, and it is exactly the kind of shortcut that would be a bug in any real interpreter.

What Golfing Took, and What It Cost

Henley published two versions on GitHub: a readable, commented interpreter and the golfed 1024-byte build. A companion write-up estimates the readable version ran to over 4,800 bytes before golfing, which is the actual size of the idea. Henley's own verdict on the process is blunt: "quite tedious," mostly because he kept toggling between the minified copy and the working version to remember what he had just changed two minutes earlier.

The golfing moves are the standard C set: single-letter identifiers everywhere, implicit int declarations (legal in C89 but gone in modern C), raw ASCII values in place of character literals, ternaries and the comma operator instead of statements, bitwise operators standing in for short-circuiting logic, and function parameters reused as scratch variables preserved on the call stack. None of that is a technique worth copying into production code. What the exercise does show is how much a pipeline needs to discard, error reporting and readable identifiers first, when a fixed byte budget becomes a hard requirement.

The table below summarizes what the constraint bought and what it traded away.

Capability Henley 1024-byte interpreter Production interpreter (CPython)
Front end Recursive descent, evaluates as it parses Tokenizer, then parser into an AST
Back end None; the C call stack is the tree Bytecode emission, then VM dispatch
Variable names Single lowercase letters only Arbitrary identifiers with scoping
Error reporting None; bad input misbehaves silently Syntax and runtime errors with locations
Source size 1,024 bytes golfed (4,800+ readable) Hundreds of thousands of lines

Each row stripped away is machinery most programmers treat as unavoidable. A symbol table that is an array rather than a hash map, no AST, no error messages: all of it becomes optional when the language subset is small enough. The trade is that the interpreter can only run programs inside that subset, and it offers nothing useful when one of them is wrong.

Single-Pass Execution at Larger Scale

The lasting lesson is a design decision, not a byte count. The fixed size cap forced a single-pass architecture, and that architecture applies more broadly: any small scripting runtime that must run a limited set of operations on throwaway input can start with an interpreter that evaluates during parse. One C file, no front-end/back-end split, no build step beyond the compiler. It is close to the smallest possible running language.

Single-Pass Execution at Larger Scale
Single-Pass Execution at Larger Scale, architecture diagram

The same idea appears in faster form elsewhere. A copy-and-patch JIT removes the interpreter dispatch loop by copying prebuilt machine-code stencils, but it still starts from the same principle: the fewer passes between source and execution, the less machinery there is to maintain. Henley's project takes that principle to its extreme, with no optimization tier at all.

The comparison also shows where single-pass stops paying off. Production CPython exists because real programs need multi-character identifiers, a tokenizer that reports clear errors, and an intermediate representation the interpreter can optimize. A compiler syllabus that starts at AST construction skips the moment when the core idea, that a language is grammar plus evaluation, first becomes concrete. Henley's interpreter restores that moment in a file short enough to read end to end in an afternoon. It runs no JIT, reports no errors, and fits no real workload, but it reveals every stage a production interpreter hides behind machinery.

Henley framed the project as writing code by hand on weekends "to feel human." The transferable lesson is narrower and more practical: a hard constraint produced a better design than good intentions would have. A vague goal of "keep it simple" would not have forced single-pass execution; the 1024-byte cap did.

More in-depth coverage from this blog on closely related topics:

Sources and References

Sources cited while researching and writing this article:

Thomas A. Anderson

Mass-produced in late 2022, upgraded frequently. Has opinions about Kubernetes that he formed in roughly 0.3 seconds. Occasionally flops, but don't we all? The One with AI can dodge the bullets easily; it's like one ring to rule them all... sort of...