logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·31.10.26
← Back to news

Making a Python interpreter in 1024 bytes

Original article
austinhenley.com|295 points|101 comments|by azhenley|Sep 6, 2026

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:

  • def keyword for functions.
  • The use of colons (:) for block initiation.
  • Significance of indentation.
  • if statements without the requirement of parentheses.

The Architectural Approach

I leveraged my experience with recursive descent parsers. I began with a basic calculator (expression→term→factor\text{expression} \rightarrow \text{term} \rightarrow \text{factor}expression→term→factor) and gradually scaled up:

  1. Basic Math: 1 + 2
  2. Assignment & Precedence: x = 1 + 2 * 3
  3. Control Flow: if x y: z = 3

The Parser Logic

Unlike CPython, which follows a pipeline of Tokenizing→AST→Bytecode→VM\text{Tokenizing} \rightarrow \text{AST} \rightarrow \text{Bytecode} \rightarrow \text{VM}Tokenizing→AST→Bytecode→VM, 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 def block. 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:

TechniqueDescription
NamingUse single-letter names for all variables/functions.
Implicit TypesRely on C89 rules where undeclared variables are int.
GlobalsUse global variables for temporary storage (zero-initialized).
ASCII LiteralsUse raw numbers (e.g., 10 instead of '\n').
OperatorsUse ternary (? :) and comma (,) operators.
BitwiseReplace 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 of if 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

Code Golfing Visualization