Rust vs Python and Where Each Language Earns Its Place
Python wins on iteration speed and libraries; Rust wins on speed, memory and shipping one binary. See where each fits, and how PyO3 lets a Python project move just its hot loop to Rust.

Python and Rust are rarely competitors, they are two ends of the same project. Python gets you an answer before lunch. Rust gets you an answer that keeps being right and fast after it ships. This post puts numbers on the difference, shows the one class of bug that separates them, and ends with how most teams actually use both. Python 3.14.6 and Rust 1.99 were used, on an Apple M-series laptop; the benchmarks are single runs of a few seconds, the best of three for the word count.
Where the speed difference is huge
The Collatz problem is a good test of raw interpreter speed: for each starting number below a million, count the steps until the sequence reaches 1, and report the longest. It is about 130 million loop iterations of integer arithmetic. The Python version:
import time
def collatz_steps(limit):
best, best_n = 0, 0
for n in range(1, limit):
x, steps = n, 0
while x != 1:
x = x // 2 if x % 2 == 0 else 3 * x + 1
steps += 1
if steps > best:
best, best_n = steps, n
return best_n, best
t = time.perf_counter()
print(collatz_steps(1_000_000))
print(f"{(time.perf_counter() - t) * 1000:.0f} ms")The Rust version has the same structure, line for line:
use std::time::Instant;
fn collatz_steps(limit: u64) -> (u64, u32) {
let (mut best, mut best_n) = (0, 0);
for n in 1..limit {
let (mut x, mut steps) = (n, 0u32);
while x != 1 {
x = if x % 2 == 0 { x / 2 } else { 3 * x + 1 };
steps += 1;
}
if steps > best {
best = steps;
best_n = n;
}
}
(best_n, best)
}
fn main() {
let t = Instant::now();
println!("{:?}", collatz_steps(1_000_000));
println!("{} ms", t.elapsed().as_millis());
}Both found the same answer, 837,799 with 524 steps, which confirms they computed the same thing.
Python 3.14.6 (837799, 524) 7238 ms
Rust 1.99 -O (837799, 524) 136 msRust was 53 times faster. This is the number people quote, and it is real for this kind of code: a tight loop of arithmetic in pure Python pays interpreter overhead on every operation, while Rust compiles the loop to a handful of machine instructions. If your hot path looks like this, Python is the wrong tool for it. It is also the least representative case in everyday Python, because nobody writes loops like this in Python on purpose; they reach for NumPy, which moves the loop into C.
Where the difference is modest
A realistic task leans on Python's libraries. Counting word frequencies in a 72 MB log file with Counter and str.split, both implemented in C inside the interpreter:
import sys
from collections import Counter
with open(sys.argv[1]) as f:
counts = Counter(f.read().split())
for word, n in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:3]:
print(word, n)
print(len(counts), "distinct")Against the Rust version of the same program from Rust versus C++, a HashMap<&str, u32> over the same file, the numbers were:
Python 3.14 Counter(text.split()) 2778 ms 698 MB peak
Rust HashMap<&str>, --release 576 ms 198 MB peakRust was 4.8 times faster and used 3.5 times less memory. Python's 698 MB comes from creating millions of small string objects, one per token, each with its own header and reference count, where Rust's &str keys point into the one buffer holding the file. The speed gap is smaller than in the loop because most of the work already ran in compiled code. The memory gap matters more than it looks: it is the difference between a job that fits on a small instance and one that needs a large one.
The honest summary of the two benchmarks is a rule: the more of your time goes into Python bytecode, the more you gain from Rust, and the more it goes into libraries written in C, the less. Measure your program before assuming either answer; profiling a Rust program and Python's cProfile both tell you where the time went.
The bug that separates them
Speed is not the best argument for Rust over Python. Correctness is. Here is a small checkout function in Python with a mistake in a branch that is rarely taken:
def total(prices, tax_rate):
subtotal = sum(prices)
if tax_rate is not None:
return subtotal + subtotal * tax_rate
return subtotal
def checkout(prices, coupon=None):
t = total(prices, 0.2)
if coupon: # rarely true, so rarely run
return t - coupon.percent_off * t # coupon is a str here: AttributeError
return t
print(checkout([10, 20]))
print(checkout([10, 20], coupon="SAVE5")) File "/private/tmp/claude-501/-Users-petehouston-Development-rusthacks-dev/92d2346c-b92a-42a3-acbf-d06dddc5871b/scratchpad/p/0037/late.py", line 10, in checkout
return t - coupon.percent_off * t # coupon is a str here: AttributeError
^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'percent_off'The first call works. The second passes a coupon as a string where the code expects an object with a percent_off attribute, and fails with an AttributeError, but only when that branch runs. In a service, that is the first customer to use a coupon, in production, after all the tests that never covered it passed. Tools like mypy catch this if you add type hints and run them in CI; the language does not require it.
The equivalent Rust:
struct Coupon {
percent_off: f64,
}
fn total(prices: &[f64], tax_rate: f64) -> f64 {
let subtotal: f64 = prices.iter().sum();
subtotal + subtotal * tax_rate
}
fn checkout(prices: &[f64], coupon: Option<&Coupon>) -> f64 {
let t = total(prices, 0.2);
match coupon {
Some(c) => t - c.percent_off * t,
None => t,
}
}
fn main() {
println!("{}", checkout(&[10.0, 20.0], None));
println!("{}", checkout(&[10.0, 20.0], Some("SAVE5")));
}error[E0308]: mismatched types
--> src/main.rs:20:49
|
20 | println!("{}", checkout(&[10.0, 20.0], Some("SAVE5")));
| ---- ^^^^^^^ expected `&Coupon`, found `&str`
| |
| arguments to this enum variant are incorrect
|It does not build. The type of the coupon is part of the function's signature, so passing a string where a &Coupon belongs is rejected at compile time, on every path, including the rarely taken one. This is the argument for Rust when software has to be right: the compiler is a test suite that cannot forget a branch. Python's counter-argument is real too: with the type annotations and tests it costs, you get most of this benefit at the price of a slower language. Python's own strengths follow.
What Python does better
- Time to a first answer. There is no compile step, a REPL and a notebook give instant feedback, and the code is shorter. For exploring data, the edit-run loop is faster than anything Rust offers.
- Library breadth. Data science, machine learning, plotting, scientific computing, web scraping and automation have mature ecosystems that Rust is still building. NumPy, pandas and PyTorch are libraries you want to use, not rewrite.
- Hiring and onboarding. More people already know it, and a script can be read by someone who does not program for a living.
- Glue. Gluing systems together with a hundred-line script is where Python is unbeatable.
- Startup is fine for both. A bare
python3 -c passtook about 24 ms and a Rust hello world 14 ms in a loop of twenty runs, and both numbers are mostly process creation.
What Rust does better
- CPU-bound work in plain code, as the Collatz run showed.
- Memory efficiency, which sets how many instances fit on a machine.
- Deployment. One static binary of about 470 KB for a hello world, with no interpreter, virtual environment or dependency resolution on the target machine.
- Correctness at scale. The compiler checks types, ownership and thread safety on every path.
- Long-running services with predictable latency and no garbage collector pause.
Using both
Most teams that adopt Rust do not leave Python. They move the one function that the profiler blames. PyO3 builds a Rust function into a Python module, and maturin packages it as a wheel:
import textstats # a Rust extension module
print(textstats.word_count("the quick brown fox"))From Python's side it is an ordinary import, with an ordinary function that happens to run at Rust speed. The walkthrough, including the boundary costs (every call crosses from Python objects to Rust values and back, so crossing once with a large batch beats crossing a million times), is in Python extensions with PyO3 and maturin. The reverse direction, embedding Python in a Rust program, exists but is rarer.
Which to pick
Start in Python when the problem is not understood yet, when the work is exploratory or when a library already does the heavy lifting. Start in Rust when the program is a service or a tool that will run for years, when a bug costs real money or when the profile shows time going into your own loops. If you cannot tell, write the first version in Python and treat the profiler's report as the decision: a function that takes 80% of the time is a candidate for Rust, and the other 20% never needs to change. The language tour is a fast way to see whether Rust's model suits you before you commit.
When this comparison does not apply
If the program spends its time waiting on a database or a network, neither language is the bottleneck, and the better choice is whichever your team maintains more easily. If the code is a one-off script that runs once, the 53-fold speed ratio saves nothing. And for machine learning training the heavy lifting is on a GPU through a C++ library, so the host language has little influence on speed; there the Python ecosystem is the deciding factor.


