{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"1024-bytes-of-c-can-fake-python-here-s-exactly-where-the-illusion-cracks-eqz4v","url":"https://api.zyvop.com/1024-bytes-of-c-can-fake-python-here-s-exactly-where-the-illusion-cracks-eqz4v","title":"1024 Bytes of C Can Fake Python. Here's Exactly Where the Illusion Cracks.","subtitle":"We compiled Austin Henley's 1,024-byte Python interpreter, ran it against real CPython, and found four undocumented bugs he never mentioned.","tldr":"Austin Henley squeezed a Python-like interpreter into 1,024 bytes of C. We verified the byte count, benchmarked it against CPython at scale, and found four silent bugs, from reserved variable names to a chained-comparison parser glitch, that never made his feature list.","keywords":["Python","C Programming","Code Golf","Interpreters","Benchmarking"],"entities":["Arpan Singh","Python","C Programming","Code Golf","Interpreters","Benchmarking","ZyVOP"],"keyTakeaways":["On September 6, 2026, Microsoft engineer Austin Z.","Henley published a Python interpreter that fits in 1,024 bytes of C.","It has def, colons, indentation, and no parentheses around if conditions."],"headings":["The claim, verified","What it actually fits","Architecture: no tokenizer stage, no AST, no bytecode","The golfing pass","First-party benchmarks","Where the illusion breaks","Is it \"real\" Python? The comment section already had this fight","Our take","Reproduce it yourself"],"outboundLinks":["https://austinhenley.com/blog/python1024.html","https://github.com/AZHenley/python1024","https://news.ycombinator.com/item?id=49591876","https://codegolf.stackexchange.com/questions/2203/tips-for-golfing-in-c","https://justine.lol/sectorlisp2/","https://sneklang.org/"],"contentText":"On September 6, 2026, Microsoft engineer Austin Z. Henley published a Python interpreter that fits in 1,024 bytes of C. It runs FizzBuzz. It handles recursion. It has def, colons, indentation, and no parentheses around if conditions. We didn't take the byte count on faith. We pulled the golfed source straight out of his post, compiled it, ran it against real CPython, and spent an afternoon finding the places it quietly disagrees with actual Python. Here's what held up, what didn't, and why the size limit itself explains both. The claim, verified We copied the exact golfed source from Henley's post into a file and checked it the same way his own README tells you to: $ wc -c python1024.c 1025 python1024.cThat 1,025 includes a trailing newline. Strip it and the source is exactly 1,024 bytes, matching the claim to the byte. It compiled clean on stock gcc 13.3.0 in -std=gnu89 mode, no special toolchain needed despite his README calling for gcc-16. We fed it his published FizzBuzz program and diffed the output against python3 running the identical file. 101 lines, byte for byte identical. The source is on GitHub under the MIT license, and at the time of writing it's sitting at 16 stars and a live Hacker News thread that started arguing about it within the hour. What it actually fits Henley's honest about the target: not Python, something that looks Pythony. His first attempt, before he'd zoomed out, was a 512-byte budget that a basic expression evaluator alone blew past. He moved the goalpost to 1,024 and started from a feature checklist instead of syntax: Single-letter integer variables and literals Assignment, and + - * % with precedence One comparison per expression: &lt; &gt; &lt;= &gt;= == Integer truthiness, if/else while and for x in range(y), both with else blocks Argument-less function definitions, including recursive calls Indentation-based blocks, with no actual scoping print of one string literal or one integer expression Comments Notice what's missing from that list on purpose: no return values, no strings as data (only as print arguments), no multi-argument functions, no error handling of any kind. The parser assumes the input is correct and marches forward regardless. We didn't just read that list, we ran it. Comments, while/for with else, and unary minus at the start of an expression all checked out clean against CPython on every case we threw at them. Henley also mentions that a FizzBuzz-only build could likely drop under 800 bytes; he stopped at 1,024 because he wanted headroom for recursion and general-purpose blocks, not just one program. Architecture: no tokenizer stage, no AST, no bytecode Real CPython runs source through five distinct stages before anything executes: tokenize, parse into an AST, optimize, compile to bytecode, then interpret that bytecode in a loop. Henley's interpreter skips every intermediate representation. It reads a character, decides what to do, and does it, right there in the parser. flowchart LR subgraph CPython[\"CPython 3.x\"] direction TB A1[\".py source\"] --&gt; A2[\"Tokenizer\"] A2 --&gt; A3[\"Parser to AST\"] A3 --&gt; A4[\"AST optimizer\"] A4 --&gt; A5[\"Bytecode compiler\"] A5 --&gt; A6[\".pyc bytecode\"] A6 --&gt; A7[\"Bytecode eval loop\"] end subgraph P1024[\"python1024\"] direction TB B1[\"char s[999] source buffer\"] --&gt; B2[\"Recursive-descent parser\"] B2 --&gt; B3[\"Executes each construct immediately\"] B3 -.-&gt;|\"loop or function call: jump position back\"| B2 endThe entire runtime state is five global variables and two arrays: char src[999]; /* Entire program, most whitespace stripped. */ int vars[256]; /* Symbol table, indexed directly by ASCII code. */ int pos; /* Next character in src. */ int ch; /* Current character. */ int line_start; /* Where the current line begins. */Because variable names are a single lowercase letter, vars[256] doesn't need hashing or lookup at all. The ASCII value of the letter is the index. x's value lives at vars[120], permanently. Loops don't compile into anything. A while or for records the buffer position of its condition, runs the body, then jumps pos back to that saved spot and re-parses the condition from scratch, every single iteration. Functions work the same way: calling one saves the caller's position, jumps to where the definition was parsed, executes the body, and restores the caller's position on return. The C call stack is the Python call stack. There's no separate frame object anywhere. flowchart TD main[\"main(): read stdin into s[999]\"] --&gt; B[\"B() run_block\"] B --&gt; I[\"I() measure indent\"] B --&gt; Y[\"Y() skip to newline\"] B --&gt; S[\"S() skip a block\"] B --&gt; Q[\"Q() print a string literal\"] B --&gt; E[\"E() one comparison\"] E --&gt; e[\"e(): plus and minus\"] e --&gt; t[\"t(): times and modulo\"] t --&gt; f[\"f(): number or variable\"] f --&gt; G[\"G() advance one character\"] B --&gt;|\"while / for: save pos, jump back each iteration\"| B B --&gt;|\"call: save pos, jump to def, run body, restore pos\"| BThat's the entire control-flow story. No AST nodes, no bytecode, no instruction pointer distinct from the parser's own cursor. The golfing pass The readable version of this interpreter runs over 4,800 bytes. Henley's own before-and-after for the addition parser shows what four and a half kilobytes of trimming looks like: /* readable */ int parse_sum(void) { int value = parse_term(); while (ch == '+' || ch == '-') { if (ch == '+') value = value + parse_term(); else value = value - parse_term(); } return value; }/* golfed */ e(){for(z=t();c-43u&lt;3;)y=44-c,z+=y*t();return z;}Same logic, and the trick is worth unpacking: c-43u&lt;3 casts to unsigned so it catches both + (43) and - (45) in one bounds check instead of two comparisons, and y=44-c turns that same character straight into +1 or -1 without a branch. He credits a decade-old Code Golf Stack Exchange thread for most of the technique library: implicit int typing under C89, globals that are zero-initialized for free, ASCII arithmetic instead of character literals, and using function parameters as scratch registers that survive on the call stack. First-party benchmarks We didn't just take the correctness claim on faith either. We ran a counting loop (n = 0; while n &lt; N: n = n + 1; print(n)) at six sizes, three runs each, averaged, on the same machine (GCC 13.3.0, CPython 3.12.3, Linux x86_64). N python1024 (avg) CPython (avg) Result 1,000 0.0022s 0.0134s python1024 ~6.1x faster 10,000 0.0026s 0.0134s python1024 ~5.2x faster 100,000 0.0111s 0.0188s python1024 ~1.7x faster 1,000,000 0.0946s 0.0619s python1024 ~1.5x slower 3,000,000 0.2708s 0.1642s python1024 ~1.6x slower 10,000,000 0.8996s 0.4720s python1024 ~1.9x slower Outputs matched exactly at every N we tested. The interesting part is the crossover between 100K and 1M iterations, and it's explained entirely by the architecture diagram above. CPython's bare startup cost alone is around 14ms on this machine, against roughly 1.6ms for the compiled python1024 binary, since there's no interpreter runtime, no import machinery, and no standard library to initialize. For small programs that startup gap dominates, and the golfed interpreter wins on pure wall clock. But CPython compiles the loop body to bytecode once and then just executes it. python1024 re-lexes and re-parses n = n + 1 from raw characters on every single pass through the loop, so its per-iteration cost stays fixed while CPython's amortizes down. Past a few hundred thousand iterations, that difference outweighs the startup advantage entirely. Where the illusion breaks The published feature list is honest about what's missing. What it doesn't mention is a set of behaviors we only found by actually running edge cases, because skipping error handling entirely turns out to hide more than it advertises. Four variable names are silently reserved. We swept all 26 lowercase letters as variable names. d, f, i, and w all break: d -&gt; BROKEN (got '0') f -&gt; BROKEN (got '') i -&gt; BROKEN (got '') w -&gt; BROKEN (got '')The statement dispatcher checks only the first character of a line to decide if it's looking at def, for, if, or while. Assign to a variable named i and the parser reads it as the start of an if, then quietly does something else with the rest of the line. Nothing crashes. It just silently doesn't do what you wrote. Multi-character identifiers alias by their first letter. The parser reads one letter, then skips the rest of any lowercase run as if it doesn't matter: count = 5 cost = 10 print(count) # real Python: 5 # python1024: 10count and cost both resolve to vars[ord('c')]. The second assignment silently overwrites the first. Undefined variables read as zero, never NameError. vars[256] is zero-initialized C memory, and there's no check for \"was this ever assigned.\" print(g) on a fresh variable prints 0 instead of raising anything. Chained comparisons don't chain, and the exact way they break depends on context. E() parses exactly one comparison operator and returns immediately, so a &lt; b &lt; c only ever evaluates a &lt; b. The code that calls E() always follows with a G() call expecting a fixed delimiter next: : after an if condition, ) after a print(...). When a leftover &lt; c sits there instead, that G() consumes the stray &lt; in place of the delimiter it expected, permanently shifting the parse position one character early for the rest of the statement. a = 5 b = 10 c = 1 print(a &lt; b &lt; c) print(999)Here that shift is harmless. The leftover c) just gets silently swallowed by the end-of-line skip that runs after every statement, so the output is a clean 1 then 999. a = 5 b = 10 c = 1 if a &lt; b &lt; c: print(99) else: print(0)Same one-character shift, worse consequence: it lands exactly where the interpreter checks whether the next line is an else at the matching indent. That check misfires, and both branches run: 99 prints, then 0 prints too, even though the condition was true and the else should have been skipped entirely. None of this is a bug in the sense of unintended crashes. It's the direct, honest cost of building zero error handling into a language surface that looks far more forgiving than it is. Is it \"real\" Python? The comment section already had this fight Henley's Hacker News thread split within the first ten replies. One commenter called it disappointing, arguing it isn't Python or anywhere close, \"not within three orders of magnitude.\" Another agreed, saying the title oversold it and should have called this Python-like rather than Python outright. A third pointed out that Python's own reputation for messy whitespace handling, mixed tabs and spaces producing different indentation levels, is exactly the kind of complexity this project sidesteps rather than solves. That criticism is fair and also slightly beside the point. Henley never claimed compatibility. He set out to make something that reads as Python to a human glancing at it, and by his own admission the syntax subset was chosen for that reason, not for completeness. The more useful comparison came from a different commenter, who pointed to Justine Tunney's SectorLISP: a Lisp interpreter, with garbage collection, that fits in 436 bytes and boots directly from a floppy disk's master boot record. SectorLISP is the more extreme size number, but Lisp's uniform s-expression grammar makes minimal parsing dramatically easier than Python's indentation-sensitive, keyword-heavy syntax. Henley wasn't just golfing bytes. He was golfing bytes against a much harder grammar to begin with. Elsewhere in the same thread, someone raised Snek, a real production language for microcontrollers with only a few kilobytes of flash and RAM, built by Keith Packard specifically because those devices are too small to run MicroPython. Snek is the honest contrast: it's Python-inspired syntax with actual error handling, a real type system, and a GPLv3 license, designed to run other people's code reliably. python1024 is a weekend art project designed to run exactly one FizzBuzz correctly and nothing more, on purpose. Our take The byte count is the hook, but it's not the finding. The finding is that a language's control-flow skeleton, if, while, for, def, indentation blocks, recursive calls, is genuinely small. Henley's B() function and its handful of helpers cover all of it in well under a page. Almost everything that makes CPython a multi-hundred-thousand-line project is generality, correctness guarantees, and the very error handling this interpreter opts out of entirely. It's not the control-flow logic itself. That's a legitimate thing to learn from a code-golf exercise, and it's worth more than the number in the title. Reproduce it yourself Every number and every bug above came from the five scripts below. Clone Henley's repo first and compile it, since we're not redistributing his source: git clone https://github.com/AZHenley/python1024 cd python1024 gcc -std=gnu89 -w python1024.c -o python1024Then drop these next to the compiled python1024 binary. The benchmark (bench.sh) produced the timing table above: #!/usr/bin/env bash # Averaged wall-clock benchmark: python1024 vs CPython. # Run this next to a compiled `python1024` binary. # # Note: python1024's golfed main() never explicitly returns, so its # process exit code is undefined garbage (harmless) -- this script # does not use `set -e` for that reason. BIN=./python1024 [ -x \"$BIN\" ] || { echo \"Build python1024 first (see README.md)\"; exit 1; } echo \"N,python1024_avg_s,cpython_avg_s,ratio,outputs_match\" for N in 1000 10000 100000 1000000 3000000 10000000; do tmp=$(mktemp) cat &gt; \"$tmp\" &lt;&lt;PYEOF n = 0 while n &lt; $N: n = n + 1 print(n) PYEOF total1=0; total2=0 for i in 1 2 3; do s=$(date +%s.%N); \"$BIN\" &lt; \"$tmp\" &gt; /tmp/o1.txt 2&gt;/dev/null; e=$(date +%s.%N) total1=$(echo \"$total1 + ($e - $s)\" | bc) s=$(date +%s.%N); python3 \"$tmp\" &gt; /tmp/o2.txt 2&gt;/dev/null; e=$(date +%s.%N) total2=$(echo \"$total2 + ($e - $s)\" | bc) done avg1=$(echo \"scale=4; $total1/3\" | bc) avg2=$(echo \"scale=4; $total2/3\" | bc) ratio=$(echo \"scale=2; $avg1/$avg2\" | bc) match=\"yes\"; [ \"$(cat /tmp/o1.txt)\" = \"$(cat /tmp/o2.txt)\" ] || match=\"no\" echo \"$N,$avg1,$avg2,${ratio}x,$match\" rm -f \"$tmp\" doneThe reserved-letter sweep (reserved_letters.sh) found d, f, i, and w: #!/usr/bin/env bash # Sweeps every lowercase letter as a variable name against python1024. # 'd', 'f', 'i', 'w' collide with def/for-if/while keyword dispatch. BIN=./python1024 [ -x \"$BIN\" ] || { echo \"Build python1024 first (see README.md)\"; exit 1; } for letter in {a..z}; do out=$(printf \"%s = 7\\nprint(%s)\\n\" \"$letter\" \"$letter\" | \"$BIN\" 2&gt;&amp;1) [ \"$out\" = \"7\" ] &amp;&amp; status=\"OK\" || status=\"BROKEN (got '$out')\" echo \"$letter -&gt; $status\" doneThe three gotcha scripts (alias.py, undefined_var.py, chained_comparison.py), each runnable directly with ./python1024 &lt; script.py: # alias.py # python1024 only looks at a variable's FIRST character. # \"count\" and \"cost\" both resolve to the same slot: 'c'. count = 5 cost = 10 print(count) # Real Python prints 5. python1024 prints 10.# undefined_var.py # No symbol table checks: an unset variable silently reads as 0. # Real Python raises NameError: name 'g' is not defined. print(g)# chained_comparison.py # E() only ever parses ONE comparison operator, not a chain. # The leftover \"&lt; c\" shifts G()'s next read by one character. # In a bare print(), that shift is swallowed by end-of-line skip. # Inside an if/else, it lands on the else-detection check instead # and both branches fire. Uncomment the second block to see it. a = 5 b = 10 c = 1 print(a &lt; b &lt; c) # Real Python: evaluates (a&lt;b) and (b&lt;c) -&gt; False # python1024: evaluates a&lt;b, shifts parse position -&gt; prints 1 # if a &lt; b &lt; c: # print(99) # else: # print(0) # Real Python: prints 0 (condition is False) # python1024: prints 99 AND 0 (else-skip check misfires)","contentHash":"sha256:614cd795f1b20b65e8699f0c192e13b92d3ca71b62b179fc58c3d996c25c778d","authorName":"Arpan Singh","authorUrl":"https://api.zyvop.com/author/arpan","authorSameAs":[],"category":null,"tags":["Python","C Programming","Code Golf","Interpreters","Benchmarking"],"audience":"Software engineers and developers building applications with Python","tone":"Practical and evidence-based engineering guidance","readingTimeMinutes":12,"wordCount":2623,"faqs":null,"primaryTopic":"Python","publishedAt":"2026-09-07T16:13:40.301Z","updatedAt":"2026-09-07T16:24:09.229Z","canonicalUrl":"https://api.zyvop.com/1024-bytes-of-c-can-fake-python-here-s-exactly-where-the-illusion-cracks-eqz4v"}