Rust vs Python

Performance, Safety, and Use Cases

Rust offers high performance and memory safety for systems programming, while Python prioritizes developer speed and ease of use for scripting and data tasks.

The speed trap and the safety net

You have a Python script that processes a CSV file. It works perfectly for 100 rows. Now you have 10 million rows, and the script takes forty minutes to run. You tried adding threads, but the Global Interpreter Lock choked the performance. You need raw speed, but you also need to keep the code readable enough that your teammate doesn't quit. You're staring at two paths: rewrite the hot loop in C and fight memory leaks, or learn Rust and fight the borrow checker.

This is the classic fork in the road. Python gets you to a working prototype in an afternoon. Rust gets you a binary that runs in seconds and uses a fraction of the memory. The trade-off isn't just speed. It's where the work happens. Python shifts complexity to runtime. Rust shifts complexity to compile time.

Managed apartments versus self-cleaning kitchens

Python gives you a garbage collector. The runtime watches your variables and cleans up memory when nothing points to them anymore. This makes coding fast because you never think about allocation. The trade-off is the runtime overhead. The collector runs in the background, pausing your program occasionally to sweep memory. It also adds a layer of indirection that slows down direct access. Every object in Python is a heap allocation with type information and reference counts attached. Accessing a value means following pointers and updating counters.

Rust takes the opposite approach. There is no garbage collector. The compiler enforces rules that guarantee memory is cleaned up exactly when it's no longer needed. You pay for this with compile-time friction. The compiler rejects code that violates ownership rules. Once the code compiles, it runs with zero runtime overhead for memory management. Values live on the stack or heap exactly where you put them. The compiler tracks lifetimes and ensures references never dangle.

Think of Python as a managed apartment. You throw trash in the bin, and the super comes by and takes it out. Sometimes the super is slow, and you have to wait. Rust is a self-cleaning kitchen. You have to put dishes away immediately, or you can't cook. The kitchen is always ready, and you never wait for the super. The rules are strict, but the result is a space that never gets cluttered.

Minimal example: Summing numbers

The difference shows up even in simple loops. Rust operates on raw data. Python operates on objects.

/// Sum integers in a vector without allocation overhead.
fn sum_numbers(numbers: &[i64]) -> i64 {
    let mut total = 0;
    // Iterate over references; no cloning or boxing occurs.
    for &n in numbers {
        total += n;
    }
    total
}

fn main() {
    let data = vec![1, 2, 3, 4, 5];
    // Pass a slice; the function borrows data without taking ownership.
    let result = sum_numbers(&data);
    println!("Sum: {}", result);
}
def sum_numbers(numbers):
    total = 0
    # Python integers are objects; iteration involves reference counting.
    for n in numbers:
        total += n
    return total

data = [1, 2, 3, 4, 5]
result = sum_numbers(data)
print(f"Sum: {result}")

In the Rust code, vec! allocates a contiguous block of memory on the heap. The i64 values sit side by side. The &data syntax creates a reference, which is just a pointer and a length. The function sum_numbers borrows the data. The compiler verifies that the reference is valid for the duration of the call. There is no hidden object header for each integer. The loop operates directly on the memory.

The Python code runs on the CPython interpreter. The list data holds pointers to integer objects. Each integer is a separate heap allocation with type metadata and a reference count. The loop dereferences each pointer and increments the reference count of the temporary variable. The interpreter also performs dynamic type checks on every operation. This flexibility costs cycles.

Convention aside: When benchmarking Rust, always use cargo run --release. Debug builds disable optimizations and run significantly slower. Comparing debug Rust to Python is misleading. Release builds enable inlining, loop unrolling, and vectorization.

Pay the compiler tax upfront. Your users won't pay the runtime tax later.

Realistic example: Processing log streams

The gap widens with complex data. Python developers often use dictionaries for structured data. Rust developers use structs with fixed layouts.

/// Parse and aggregate log entries efficiently.
struct LogEntry {
    level: u8,
    message: String,
}

fn process_logs(raw_logs: &[&str]) -> usize {
    let mut error_count = 0;
    // Pre-allocate capacity to avoid reallocations during push.
    let mut errors = Vec::with_capacity(raw_logs.len());

    for line in raw_logs {
        // Manual parsing avoids allocation overhead of JSON libraries.
        if line.starts_with("ERROR") {
            error_count += 1;
            errors.push(line.to_string());
        }
    }
    error_count
}

fn main() {
    let logs = vec!["INFO start", "ERROR fail", "INFO end"];
    let count = process_logs(&logs);
    println!("Found {} errors", count);
}

Rust lets you define a LogEntry struct with exact memory layout. The level field is a single byte. The message is a pointer to heap data. You can parse strings into slices without copying data. The Vec::with_capacity call tells the allocator how much space to reserve upfront. This prevents the vector from resizing multiple times as it grows. Resizing requires allocating a new block and copying data. Pre-allocation eliminates that cost.

Python dictionaries are hash maps. They offer flexibility but add hashing overhead and memory fragmentation. Accessing a field requires a hash lookup. Rust's struct fields are accessed via fixed offsets. The compiler inlines these accesses. You also get null safety for free. Python has None. Rust has Option<T>. The compiler forces you to handle the absence case. You never get a null pointer exception at runtime.

Convention aside: The Rust community relies heavily on clippy, a linter that ships with the toolchain. It catches idiomatic issues that the compiler misses. Running cargo clippy before committing is standard practice. It suggests improvements like using .is_empty() instead of .len() == 0.

The borrow checker isn't trying to stop you. It's trying to save you from a segfault at 3 AM.

Pitfalls and compiler errors

The biggest hurdle is the borrow checker. You'll hit errors that feel arbitrary until you understand the invariants.

fn main() {
    let mut data = vec![1, 2, 3];
    // Create an immutable reference.
    let ref1 = &data;
    // Attempt to mutate while ref1 is active.
    // This triggers E0502.
    data.push(4);
    println!("{}", ref1[0]);
}

The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The rule is simple: you can have many immutable references or one mutable reference, but not both at the same time. If ref1 points to data, and data reallocates or changes, ref1 becomes invalid. The compiler stops this.

You'll also see E0382 (use of moved value). Rust moves ownership by default. If you pass a String to a function, the function owns it. You can't use it afterwards unless you clone or borrow.

Convention aside: The community convention is to accept &str instead of &String in function parameters. This accepts string slices from literals, String objects, or other sources without forcing the caller to allocate. It makes your API more flexible.

Trust the borrow checker. It usually has a point.

Decision: Rust or Python?

Pick the tool that matches the bottleneck. If the bottleneck is your time, Python wins. If the bottleneck is the CPU, Rust wins.

Use Python when you need rapid prototyping and have access to a rich ecosystem of libraries for data science, web frameworks, or scripting. Use Python when development speed matters more than execution speed, and the workload is I/O bound rather than CPU bound. Use Python when your team knows Python and the project timeline is tight. Use Python when you need to glue together existing services and don't care about memory overhead.

Use Rust when you need predictable low latency and high throughput for CPU-bound tasks. Use Rust when you are building systems software, CLI tools, or libraries that other languages will call via FFI. Use Rust when memory safety is critical and you cannot tolerate garbage collector pauses. Use Rust when you want to eliminate entire classes of bugs like null pointer dereferences and buffer overflows at compile time. Use Rust when you are building a long-lived service where stability matters more than initial development speed.

Counter-intuitive but true: the more you use Rust, the less you think about memory. The compiler internalizes the rules until they become second nature.

Where to go next