Rust vs Python

A Compiler & Interpreter Deep Dive

An interactive exploration of every stage in the Rust compilation pipeline and Python execution model. Click, hover, and explore to understand how your code transforms from source text to running program.

Explore Rust Pipeline Explore Python Pipeline
02

High-Level Pipeline Overview

Click any stage to jump to its detailed explanation.

Rust Compilation Pipeline

Source Lexer Parser AST HIR MIR LLVM IR Machine Code

Python Execution Pipeline

Source Lexer Parser AST Bytecode PVM
Key Difference: Rust compiles ahead-of-time (AOT) through multiple intermediate representations down to native machine code. Python compiles to bytecode at import time, then interprets that bytecode in a virtual machine at runtime.
03

Source Code

The same Fibonacci algorithm in both languages. This is the starting point of each pipeline.

Rust
fn fib(n: u64) -> u64 {
    if n <= 1 {
        return n;
    }
    fib(n - 1) + fib(n - 2)
}

fn main() {
    let result = fib(10);
    println!("{}", result);
}
Python
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

result = fib(10)
print(result)
04

Lexical Analysis / Tokenization

The lexer breaks source text into a stream of tokens — the smallest meaningful units.

Rust Tokens
fn fib ( n : u64 ) -> u64 { if n <= 1 { return n ; } fib ( n - 1 ) + fib ( n - 2 ) }
Python Tokens
def fib ( n ) : NEWLINE INDENT if n <= 1 : NEWLINE INDENT return n NEWLINE DEDENT return fib ( n - 1 ) + fib ( n - 2 ) DEDENT
Key Difference: Python's lexer generates INDENT, DEDENT, and NEWLINE tokens because whitespace is syntactically significant. Rust uses braces {} and semicolons, so whitespace is ignored.

Token Type Comparison

CategoryRust ExamplesPython Examples
Keywordsfn, let, if, return, mut, pubdef, if, return, class, import
Identifiersfib, n, resultfib, n, result
Literals1, 2, "hello"1, 2, "hello"
Operators+, -, <=, ->+, -, <=, **
Delimiters{ } ( ) ; :( ) : INDENT DEDENT
Type Annotationsu64, String, Vec<T>Optional hints: int, str
05

Parsing → Abstract Syntax Tree

The parser consumes tokens and builds a tree structure representing the program's syntax. Click nodes to expand/collapse.

Rust AST
Python AST
Key Difference: Rust's AST includes type annotations and lifetime markers as first-class nodes. Python's AST is simpler — no type info is required, and the structure directly mirrors the dynamic semantics. Rust uses a formal grammar (LL/recursive descent), while CPython uses a PEG parser since 3.9.
06

Rust-Specific Compilation Stages

Rust transforms code through four additional intermediate representations before producing machine code.

Stage 1: AST → HIR (High-Level IR)

Desugaring, name resolution, and type checking happen here. Syntactic sugar like for loops become lower-level constructs.

Before (AST)

for i in 0..10 {
    println!("{}", i);
}

After (HIR) — Desugared

// for loop desugared to loop + match
{
    let mut iter = IntoIterator::into_iter(0..10);
    loop {
        match Iterator::next(&mut iter) {
            Some(i) => { println!("{}", i); }
            None => break,
        }
    }
}
Type checking verifies that all types are consistent. The compiler infers types where possible and checks that trait bounds are satisfied.
Stage 2: HIR → MIR (Mid-Level IR)

MIR is a control-flow graph used for borrow checking, optimization, and code generation.

MIR for fib()

// Simplified MIR representation
bb0: {
    _2 = Le(_1, const 1_u64);
    switchInt(_2) -> [0: bb2, otherwise: bb1];
}
bb1: {
    _0 = _1;
    return;
}
bb2: {
    _3 = Sub(_1, const 1_u64);
    _4 = fib(_3);
    _5 = Sub(_1, const 2_u64);
    _6 = fib(_5);
    _0 = Add(_4, _6);
    return;
}

Borrow Checker at Work

// This would FAIL borrow checking:
let mut v = vec![1, 2, 3];
let first = &v[0];  // immutable borrow
v.push(4);       // mutable borrow ERROR!
println!("{}", first);

// Error: cannot borrow `v` as mutable
// because it is also borrowed as immutable
The borrow checker enforces that you cannot have a mutable reference while immutable references exist. This prevents data races at compile time.
Stage 3: MIR → LLVM IR

MIR is lowered to LLVM's intermediate representation for platform-independent optimization.

; LLVM IR for fib function (simplified)
define i64 @fib(i64 %n) {
entry:
  %cmp = icmp ule i64 %n, 1
  br i1 %cmp, label %base, label %recurse
base:
  ret i64 %n
recurse:
  %n1 = sub i64 %n, 1
  %f1 = call i64 @fib(i64 %n1)
  %n2 = sub i64 %n, 2
  %f2 = call i64 @fib(i64 %n2)
  %sum = add i64 %f1, %f2
  ret i64 %sum
}
LLVM applies dozens of optimization passes: inlining, constant folding, loop unrolling, vectorization, dead code elimination, and more. The optimized IR is then lowered to machine-specific code.
Stage 4: LLVM IR → Machine Code

LLVM's backend generates native assembly for the target architecture (e.g., x86-64).

; x86-64 assembly (simplified)
fib:
    cmp     rdi, 1
    jbe     .base
    push    rbx
    mov     rbx, rdi
    lea     rdi, [rbx - 1]
    call    fib
    mov     rcx, rax
    lea     rdi, [rbx - 2]
    call    fib
    add     rax, rcx
    pop     rbx
    ret
.base:
    mov     rax, rdi
    ret
The final binary is a standalone executable with no runtime dependency. It links against system libraries and can be distributed without requiring a Rust installation on the target machine.
07

Python-Specific Execution Stages

Python compiles to bytecode, then interprets it in the Python Virtual Machine.

Stage 1: AST → Bytecode

CPython compiles the AST to bytecode instructions stored in .pyc files. Use dis.dis() to inspect.

# Output of dis.dis(fib)
  2     0 LOAD_FAST                0 (n)
        2 LOAD_CONST               1 (1)
        4 COMPARE_OP              1 (<=)
        6 POP_JUMP_IF_FALSE       12

  3     8 LOAD_FAST                0 (n)
       10 RETURN_VALUE

  4    12 LOAD_GLOBAL              0 (fib)
       14 LOAD_FAST                0 (n)
       16 LOAD_CONST               1 (1)
       18 BINARY_SUBTRACT
       20 CALL_FUNCTION            1
       22 LOAD_GLOBAL              0 (fib)
       24 LOAD_FAST                0 (n)
       26 LOAD_CONST               2 (2)
       28 BINARY_SUBTRACT
       30 CALL_FUNCTION            1
       32 BINARY_ADD
       34 RETURN_VALUE
Stage 2: Bytecode → PVM (Stack Machine)

The Python Virtual Machine is a stack-based interpreter. Click "Step" to execute bytecode instructions and watch the stack change.

Bytecode Instructions

Stack

08

Type System Comparison

Static typing (Rust) catches errors at compile time. Dynamic typing (Python) defers checks to runtime.

Rust — Static Typing
let x: i32 = 5;
let y: String = "hello".to_string();

// Compile-time error:
let z = x + y;
// ERROR: cannot add `String` to `i32`
//   mismatched types
//   expected `i32`, found `String`
Caught at compile time. The program will not compile. No runtime cost for type checking — types are erased after compilation.
Python — Dynamic Typing
x = 5
y = "hello"

# Runtime error:
z = x + y
# TypeError: unsupported operand type(s)
#   for +: 'int' and 'str'

# But this is valid:
x = "now I'm a string"  # rebinding is fine
Caught at runtime. The program starts executing and crashes only when the problematic line is reached. Variables can change type freely.
AspectRust (Static)Python (Dynamic)
When types checkedCompile timeRuntime
Type annotationsRequired (with inference)Optional (hints only)
Variable rebinding typeNot allowedAllowed freely
GenericsMonomorphized at compile timeDuck typing (no need)
Runtime overheadZero — types erasedEvery value carries type info
09

Memory Management

Rust uses ownership with zero-cost abstractions. Python uses reference counting + garbage collection.

Rust — Ownership Model
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 is MOVED to s2
    // println!("{}", s1);  // ERROR: s1 no longer valid
    println!("{}", s2);  // OK
} // s2 dropped, memory freed

Ownership Transfer

s1
"hello"
s2
(empty)
Rules: (1) Each value has exactly one owner. (2) When the owner goes out of scope, the value is dropped. (3) You can borrow (immutable & or mutable &mut) but not both simultaneously.
Python — Reference Counting + GC
import sys

a = [1, 2, 3]
print(sys.getrefcount(a))  # 2 (a + getrefcount arg)

b = a  # b references same object
print(sys.getrefcount(a))  # 3

del b  # refcount decremented
print(sys.getrefcount(a))  # 2

Reference Count

[1,2,3]
refcount: 1
a → attached
b → none
How it works: Each object has a reference count. When it reaches 0, memory is freed immediately. A cyclic garbage collector handles reference cycles that refcounting alone can't solve.
10

Error Handling Comparison

Rust enforces explicit error handling at compile time. Python uses exceptions at runtime.

Rust — Result / Option
use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    let content = fs::read_to_string("config.toml")?;
    Ok(content)
}

fn main() {
    match read_config() {
        Ok(cfg) => println!("Config: {}", cfg),
        Err(e) => eprintln!("Error: {}", e),
    }
}
Compile-time enforcement: You must handle the Result. The ? operator propagates errors elegantly. Unhandled results produce compiler warnings.
Python — Exceptions
def read_config():
    with open("config.toml") as f:
        return f.read()

try:
    cfg = read_config()
    print(f"Config: {cfg}")
except FileNotFoundError as e:
    print(f"Error: {e}")
# Forgetting try/except? Program crashes at runtime.
Runtime enforcement: Nothing forces you to handle exceptions. Unhandled exceptions crash the program. Any function might raise — there's no indication in the signature.
AspectRustPython
Error mechanismResult<T, E> / Option<T>Exceptions (try/except)
When enforcedCompile timeRuntime
Can ignore errors?Only explicitly (.unwrap())Yes, silently
Error propagation? operatorAutomatic stack unwinding
Null handlingOption<T> — no nullNone + AttributeError
11

Performance Comparison

Approximate relative comparisons for typical workloads. Lower is better for all metrics except throughput.

Execution Speed (Fibonacci 40)

Rust
~0.3s
Python
~25s

Memory Usage (Typical Web Server)

Rust
~5 MB
Python
~50 MB

Startup Time

Rust
~1ms
Python
~30ms

Compilation / Interpretation Time

Rust
Seconds-minutes
Python
Instant
Trade-off: Rust pays upfront with longer compile times to produce highly optimized machine code. Python starts instantly but pays at runtime for interpretation overhead. Each model suits different use cases — Rust for performance-critical systems, Python for rapid development and scripting.
12

Concurrency Model

Rust guarantees thread safety at compile time. Python has the GIL limiting true parallelism.

Rust — Fearless Concurrency
use std::thread;

fn main() {
    let mut handles = vec![];
    for i in 0..4 {
        handles.push(thread::spawn(move || {
            println!("Thread {} running", i);
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
}

Thread Execution

Thread 0
work
Thread 1
work
Thread 2
work
Thread 3
work
True parallelism. All threads execute simultaneously on separate CPU cores. The ownership system prevents data races at compile time.
Python — GIL Constrained
import threading

def work(i):
    print(f"Thread {i} running")

threads = []
for i in range(4):
    t = threading.Thread(target=work, args=(i,))
    t.start()
    threads.append(t)
for t in threads:
    t.join()

Thread Execution (GIL)

Thread 0
GIL
GIL
Thread 1
GIL
GIL
Thread 2
GIL
Thread 3
GIL
Only one thread holds the GIL at a time. CPU-bound threads take turns, not true parallelism. Use multiprocessing for CPU-bound tasks, or asyncio for I/O-bound tasks.
13

Summary & Key Takeaways

FeatureRustPython
Execution ModelAOT compiled to native codeInterpreted bytecode (PVM)
Type SystemStatic, strong, inferredDynamic, strong, duck-typed
MemoryOwnership + borrowing (no GC)Reference counting + GC
Null SafetyOption<T> — no nullNone + runtime errors
Error HandlingResult<T,E> — explicitExceptions — implicit
ConcurrencyFearless — compile-time safetyGIL limits parallelism
PerformanceNear C/C++ speed10-100x slower (CPU-bound)
Compile TimeSlow (seconds to minutes)Instant (interpreted)
Learning CurveSteep (ownership, lifetimes)Gentle (beginner-friendly)
Ecosystemcrates.io (~150k crates)PyPI (~500k packages)
Best ForSystems, CLI, WebAssembly, embeddedScripting, ML/AI, web backends, data

When to Use Rust

  • Performance-critical applications
  • Systems programming (OS, drivers, embedded)
  • WebAssembly modules
  • CLI tools that need to be fast
  • Concurrent/parallel processing
  • When correctness guarantees matter

When to Use Python

  • Rapid prototyping and scripting
  • Machine learning and data science
  • Web development (Django, Flask, FastAPI)
  • Automation and DevOps
  • Teaching and learning programming
  • When development speed beats runtime speed

Built by Dhruv Joshi. Inspired by Transformer Explainer.

Rust and Python logos and names are trademarks of their respective owners.