Making a Python interpreter in 1024 bytes
Crafting a Python Interpreter within 1024 Bytes
By Henley
Tools builder | austin.henley@microsoft.com | @austinzhenley
[Home] | [Publications] | [Teaching] | [Blog]
Date: 9/6/2026
"To feel human, I write code by hand on the weekends."
The objective was simple yet daunting: construct a functional Python interpreter using no more than 1024 bytes of standard C code. Initially, I thought 512 bytes might work, but I quickly realized that was too ambitious.
The Target Program
To test the interpreter, I aimed to run a classic FizzBuzz implementation:
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()
Fitting the entire Python specification into a kilobyte is impossible. Instead, I focused on a subset that captures the "essence" of Python:
defkeyword for functions.- The use of colons (
:) for block initiation. - Significance of indentation.
ifstatements without the requirement of parentheses.
The Architectural Approach
I leveraged my experience with recursive descent parsers. I began with a basic calculator () and gradually scaled up:
- Basic Math:
1 + 2 - Assignment & Precedence:
x = 1 + 2 * 3 - Control Flow:
if x y: z = 3
The Parser Logic
Unlike CPython, which follows a pipeline of , this interpreter is much leaner.
- Storage: A fixed-length array of 999 characters stores the source code.
- Symbols: A single array manages all variable and function names.
- Execution: Expressions are executed during parsing.
For instance, the addition logic looks like this in a readable format:
int parse_sum(void) {
int value = parse_term();
while (ch == '+' || ch == '-') {
if (ch == '+') value = value + parse_term();
else value = value - parse_term();
}
return value;
}
Assumptions for Brevity
To save space, the interpreter makes several "optimistic" assumptions:
- Keywords are spelled perfectly.
- Token boundaries are correct.
- Whitespace is mostly irrelevant.
- Variable Constraint: Names must be a single lowercase character. This allows for direct symbol table indexing:
if (ch > 96) { value = vars[ch]; next(); }
Control Flow Magic
The interpreter handles blocks by monitoring indentation levels. It uses the C call stack to manage recursion.
void run_block(int min_indent) {
for (;;) {
int indent = read_indent();
if (ch == '\n') continue;
if (indent < min_indent || ch == 0) {
pos = line_start;
return;
}
// ... execute statement ...
}
}
Loops and Functions
Since there is no intermediate representation (IR) or bytecode, the interpreter "jumps" through the source code:
- Loops: The interpreter records the position of the condition. After the body executes, it jumps back to that pointer and re-parses the source.
- Functions: The symbol table stores the source position of the
defblock. When called, the current position is saved, the parser jumps to the function body, and then returns to the saved location.
The Art of Code Golfing
To shrink the binary, I turned to the "ancient" wisdom of Stack Overflow and applied several aggressive optimization techniques:
| Technique | Description |
|---|---|
| Naming | Use single-letter names for all variables/functions. |
| Implicit Types | Rely on C89 rules where undeclared variables are int. |
| Globals | Use global variables for temporary storage (zero-initialized). |
| ASCII Literals | Use raw numbers (e.g., 10 instead of '\n'). |
| Operators | Use ternary (? :) and comma (,) operators. |
| Bitwise | Replace logical operators with bitwise ones where possible. |
Comparison: Readable vs. Golfed
Example 1: Summation
- Readable:
parse_sum(void) { ... } - Golfed:
e(){for(z=t();c-43u 3;)y=44-c,z+=y*t();return z;}
Example 2: End-of-Line Skip
- Readable:
void skip_to_eol(void) { if (ch != 0 && ch != '\n') { next(); skip_to_eol(); } } - Golfed:
Y(){c c-10 Y(G());}(Uses the comma operator and implicit truthiness).
Final Refinements
To hit the 1024-byte target, I had to implement a "cutting list":
- Remove complex comparison expressions.
- Rely on "truthiness" (e.g.,
if n%15:instead ofif n%15 != 0:). - Strip all non-essential whitespace.
If the only goal were to run FizzBuzz, I suspect I could drop below 800 bytes.
The Final Source Code
Here is the completed, golfed C implementation:
char s[999];v[256],p,c,x,y,z,w,u;G(){return c=s[p++];}I(){for(u=p;G()==32;);return p-u;}Y(){c c-10 Y(G());}f(){x=0;if(G() 96)x=v[c],G();for(;c-48u 10;G())x=x*10+c-48;return x;}t(g,h){for(g=f();c==42|c==37;)h=c,g=h-42?g%f():g*f();return g;}e(){for(z=t();c-43u 3;)y=44-c,z+=y*t();return z;}E(a,q){a=e();if(c-60u 2)return a;w=c-61;q=G()==61;p-=!q;x=e();return w?(a-x)*w -q:a==x;}S(i){for(;I() i|c==10;)Y();p=u;}Q(){for(G();G()-34;)putchar(c);G();}B(i,q,j,k,a,m,n){for(;;){j=I();if(c==10)continue;if(j i|!c){p=u;return;}if(c==119|c==105|c==102){k=c;k-102?p+=k/4-25:(p+=2,m=G(),p+=8,v[m]=0);q=p;for(;;){a=k-102?E():v[m] E();p+=k==102;G();if(!a){S(j);break;}B(j+1);if(k==105)break;k-102||v[m]++;p=q;}I()-j|c-101?p=u:(p+=4,G(),a?S(j):B(j+1));}else if(c==100){p+=2;k=G();Y();v[k]=p;S(j);}else{if(c 96){k=c