How-To Guides
Technical Primer · Visual Edition
Rust for a Python Developer
Why it's fast, what maps to what, and how you'd actually use it
Diagrams over paragraphs. Keep writing Python — drop to Rust for the 5% that's slow.
TL;DR — the four reasons, and the one move
Rust deletes four things CPython pays for at runtime: interpretation, the GIL, the garbage collector, the runtime itself. None of it matters for I/O-bound or glue code — most of what you write. So don't rewrite: use PyO3 to move one hot function into Rust and import it.
Versions as of this report: Rust 1.97.0 (9 Jul 2026), edition 2024, six-week release train · PyO3 0.29.0 · maturin 1.14.1 · tokio 1.52.3.
1. Why Rust Is Fast — Four Diagrams
Each diagram removes one runtime cost — read as "what happens per unit of work."
Diagram A — What runs your loop
▲ the four amber stages repeat every single iteration — that's the interpreter tax.
▲ everything grey happens once, at cargo build. The loop itself is bare instructions on raw i64s.
PyObject whose type is inspected at runtime. Rust optimises ahead of time, so an i64 in a loop is a register, not an object. Source: the Rust Book; CPython docs.Diagram B — Four cores, one CPU-bound job
CPython (default build) — the GIL
Four threads take turns holding one lock. Wall-clock ≈ one core's worth. Grey = paid-for silicon idling.
Rust — no GIL
Threads run at once; the compiler statically rejects the data races. ≈4× the work, same wall-clock.
Diagram C — When does memory get freed?
Every assignment bumps a counter at runtime. Cycles need a collector pass that can stop the world.
cost: runtime, always
One owner. The compiler knows the line the owner dies on, and frees there.
cost: compile time, once
Many readers or one writer, never both. That one rule makes Diagram B provably safe.
cost: compile time, once
A number is a number. In Python it's a heap object with header, type pointer and refcount.
cost: ~free
Chart D — Measured gap on CPU-bound work (lower is better)
Where Rust buys you nothing: a FastAPI endpoint waiting on Postgres is idle, not slow — Rust makes the waiting no shorter. Glue, cron scripts and orchestration are dominated by the calls they make. And if your hot loop is already NumPy, Polars or a CUDA kernel, you are already running native code. Rust pays off only where Python itself executes the inner loop.
2. Primitives — Rust ↔ Python, Side by Side
Mostly a renaming exercise. Exactly one row has no Python equivalent — ownership. That row is the whole learning curve.
x = 5 x = 6 # always rebindable CONST = 3.14 # convention only
Nothing stops any reassignment.
let x = 5; // x = 6; // ✗ compile error let mut y = 5; y = 6; // ✓ opt in to mutation const PI: f64 = 3.14;
Mutability is a deliberate, visible choice.
def add(a: int, b: int) -> int:
return a + b
# hints are optional, unenforced
add("a", "b") # runs fine!Type errors surface in production.
fn add(a: i64, b: i64) -> i64 {
a + b // no `return`, no `;`
}
let n = 5; // inferred i32
// add("a", "b") // ✗ won't compileLike mypy, but mandatory and never wrong.
a = [1, 2, 3] b = a # both point at one list b.append(4) print(a) # [1, 2, 3, 4] — surprise! # freed when refcount hits 0
Aliasing is invisible; the GC cleans up whenever.
let a = vec![1, 2, 3];
let b = a; // MOVED
// println!("{:?}", a); // ✗ a is gone
let c = vec![1, 2, 3];
let r = &c; // borrow: read-only
let d = c.clone(); // explicit copy
// freed exactly where `c` scope endsLifetimes (&'a T) just prove a borrow outlives nothing it shouldn't.
try:
v = int(s)
except ValueError:
v = 0
# nothing forces you to catch itInvisible in the signature; escapes at runtime.
let v: i64 = s.parse().unwrap_or(0);
fn get(s: &str) -> Result<i64, ParseIntError> {
let n = s.parse()?; // ? = propagate
Ok(n)
}
// Option<T> replaces None — no NoneType errorsFailure is in the type; the compiler makes you handle it.
@dataclass
class Dog:
name: str
def speak(self): return "woof"
# any object with .speak() works
def noise(x): return x.speak()Implicit protocol; fails at call time.
struct Dog { name: String }
trait Speak { fn speak(&self) -> &str; }
impl Speak for Dog {
fn speak(&self) -> &str { "woof" }
}
fn noise<T: Speak>(x: &T) -> &str { x.speak() }
enum Msg { Quit, Move { x: i32 } } // real sum typeTraits = duck typing, compile-checked. No inheritance.
xs = [1, 2, 3] # list
d = {"a": 1} # dict
s = {1, 2} # set
t = (1, "a") # tuple
txt = "hi" # strlet xs = vec![1, 2, 3]; // Vec<i32>
let mut d = HashMap::new(); // HashMap
d.insert("a", 1);
let s: HashSet<i32> = HashSet::new();
let t = (1, "a"); // tuple
let txt = String::from("hi"); // vs &strOne genuine wrinkle: owned String vs borrowed &str.
evens = [x * 2 for x in xs if x % 2 == 0] total = sum(xs) f = lambda a: a + 1
let evens: Vec<i32> = xs.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.collect();
let total: i32 = xs.iter().sum();
let f = |a: i32| a + 1;
// rayon: .iter() → .par_iter() = all cores
Lazy like generators — and compile to a plain loop.
python -m venv .venv pip install requests # pyproject.toml / requirements.txt pytest ; black . ; ruff check .
Several tools, several configs.
cargo new myapp cargo add serde # → Cargo.toml cargo build --release cargo test ; cargo fmt ; cargo clippy
One tool, one Cargo.toml, lockfile by default.
async def fetch(u):
async with session.get(u) as r:
return await r.text()
asyncio.run(fetch(url))Cooperative, single-threaded, GIL-bound.
async fn fetch(u: &str) -> Result<String> {
reqwest::get(u).await?.text().await
}
#[tokio::main]
async fn main() { fetch(url).await; }Same async/await shape — but a work-stealing multi-thread runtime, so tasks span real cores.
| Concept | Python analogue | Effort |
|---|---|---|
| Iterators, closures, collections | Comprehensions, lambdas, list/dict/set | Trivial — a rename |
| cargo / crates.io | pip + venv + pyproject | Trivial — strictly nicer |
| Static types | Type hints, enforced | Easy if you use mypy |
| Result / Option | Exceptions, None | A real re-think |
| Traits & enums | Protocols / ABCs; no sum type | A real re-think |
| async | asyncio (same keywords) | Syntax easy, ecosystem fiddly |
| Ownership · borrowing · lifetimes | None. Nothing. New. | ★ The hump |
Not a personal failing: Rust's own 2026 vision research names the borrow checker, traits/generics and async as the three hardest things, and 31% of non-users cite perceived difficulty as their main reason for skipping Rust. Source: Rust Blog, "What we heard about Rust's challenges" (Mar 2026).
3. Getting Started — The Path
Six steps. Step 4 is the wall — everyone hits it, and it's temporary.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh — brings rustc, cargo, clippy, fmt. New stable every 6 weeks via rustup update.cargo new hello && cd hello && cargo run. Add rust-analyzer to your editor — inline errors make the borrow checker teachable, not cryptic..clone() freely, use Vec not references, ignore lifetimes until forced. Make it compile now; optimise later.cargo install rustlings → rustlings init → rustlings. ~100 small broken programs you fix, watched and re-run. Where ownership stops being theory.Chart E — Expected productivity, honestly (qualitative)
▸ relative to your Python speed on the same task
Your first week:
rustupin,cargo runprints Hello — day 1- rust-analyzer live in your editor — day 1
- The Book ch. 1–4, ownership twice — days 2–4
rustlings init, first 20 exercises — days 3–7- One CLI that reads a file and counts something — day 6
- Trigger three borrow-checker errors deliberately, read them properly — day 7
4. How You'd Actually Use It
Four on-ramps. Only one is the right first move.
import it. Reversible in an afternoon.wasm-pack + wasm-bindgen → wasm32-unknown-unknown. Native-ish compute in the browser, callable from JS.uv, ruff, Polars, Pydantic v2 core are Rust under a Python skin. Zero cost.Workflow — the whole PyO3 loop, end to end
pip install maturin maturin new -b pyo3 fastbits cd fastbits maturin develop --release # builds the Rust ext + installs it # into your active venv
use pyo3::prelude::*;
#[pyfunction]
fn sum_sq(xs: Vec<i64>) -> i64 {
xs.iter().map(|x| x * x).sum()
}
#[pymodule]
fn fastbits(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_sq, m)?)?;
Ok(())
}import fastbits fastbits.sum_sq([1, 2, 3]) # → 14 # same signature as the Python # version it replaced. Callers # never know.
maturin build --release # → a wheel. pip install it, # publish to PyPI, or vendor # it into your image.
PyO3 0.29 also supports free-threaded CPython via the abi3t stable ABI.
ctypes, no hand-written build scripts — maturin handles the wheel, ABI and venv install. Sources: pyo3.rs guides; maturin.rs.Where it might pay off in a typical Python stack
Illustrative — none of this argues for a rewrite. Profile first; the candidate is always "Python executes the inner loop."
| Candidate | Why it might qualify | Verdict |
|---|---|---|
| A compute-hot pipeline stage | Pure-Python loops over many items (scoring, parsing) rather than NumPy or a DB. | PyO3 candidate — profile it |
| An audio / real-time latency path | Per-frame DSP or VAD with a hard latency tail: where GC pauses and the GIL bite. | PyO3 or sidecar — check p99 |
| Any FastAPI endpoint | Waiting on Postgres, an LLM API, the network. | No — I/O-bound |
| Agent orchestration / glue | Dominated by the calls it makes; iteration speed is the asset. | No — keep Python |
| Already NumPy / Polars / CUDA | The inner loop is already native. | No — win already banked |
5. Should I Reach for Rust? — Decision Tree
Answer top to bottom. The first "Yes" is your answer.
Rust isn't a replacement for Python. It's a pressure valve for the 5% Python is bad at.
Its speed isn't cleverness — it's subtraction. No interpreter between your loop and the CPU, no lock serialising your threads, no collector deciding when to pause. You pay for that once, at compile time, in borrow-checker arguments. For an endpoint waiting on a database, that's a bad trade. For one function pinning a core, it's the best trade in software. Run maturin new -b pyo3, move one function, measure. If it's not 10× faster you picked the wrong function — and you've lost an afternoon, not a quarter.
References
All URLs accessed 17 July 2026. primary = official project/vendor documentation; secondary = third-party or vendor-marketing claims, corroborating only.
Rust — language, toolchain, learning
- primary Rust 1.97.0 release announcement (9 Jul 2026) — blog.rust-lang.org/2026/07/09/Rust-1.97.0
- primary Rust Edition Guide — Rust 2024 is the current edition — doc.rust-lang.org/edition-guide/rust-2024
- primary Install Rust / rustup one-liner — rustup.rs · rust-lang.org/tools/install
- primary The Rust Programming Language ("the Book") — doc.rust-lang.org/book · ownership: ch. 4
- primary Rustlings — install & run — rustlings.rust-lang.org
- primary Rust by Example — doc.rust-lang.org/rust-by-example
- primary "What we heard about Rust's challenges" (borrow checker / traits / async; 31% difficulty figure) — blog.rust-lang.org/2026/03/20/rust-challenges
- primary "The many journeys of learning Rust" (vision doc) — blog.rust-lang.org/2026/06/25/vision-doc-journeys-to-learning-rust
- secondary releases.rs — stable version tracker — releases.rs
Python interop — PyO3, maturin
- primary PyO3 releases — 0.29.0 (11 Jun 2026), abi3t, Python 3.15 support — github.com/PyO3/pyo3/releases
- primary PyO3 guide — Python modules (
#[pymodule]/Bound<'_, PyModule>) — pyo3.rs/main/module - primary PyO3 guide — Python functions (
#[pyfunction]) — pyo3.rs/main/function.html - primary PyO3 free-threading (no-GIL) support — pyo3.rs/v0.29.0/free-threading
- primary PyO3 — supporting multiple Python versions — pyo3.rs/main/building-and-distribution/multiple-python-versions
- primary maturin 1.14.1 on PyPI — pypi.org/project/maturin · docs: maturin.rs · changelog: maturin.rs/changelog.html
Python runtime — the GIL
- primary PEP 779 — free-threaded Python officially supported (Phase II, 3.14) — peps.python.org/pep-0779
- primary PEP 703 — making the GIL optional — peps.python.org/pep-0703
- primary Python free-threading HOWTO (opt-in status, caveats) — docs.python.org/3/howto/free-threading-python.html
Benchmarks & energy
- secondary Computer Language Benchmarks Game — n-body (Rust 2.19 s vs CPython ≈360 s) — benchmarksgame-team.pages.debian.net/…/nbody.html
- secondary Benchmarks Game — binary-trees (Rust 1.07 s vs CPython 33.37 s) — benchmarksgame-team.pages.debian.net/…/binarytrees.html
- secondary Benchmarks Game — Rust vs Python index — benchmarksgame-team.pages.debian.net/…/rust-python3.html
- secondary Pereira et al., "Energy Efficiency across Programming Languages" (SLE'17) — greenlab.di.uminho.pt/…/paperSLE.pdf
- secondary "It's Not Easy Being Green" — follow-up re-evaluation (arXiv 2410.05460) — arxiv.org/html/2410.05460v1
Rust-backed Python tooling & ecosystem
- secondary uv — 10–100× vs pip (vendor claim) — github.com/astral-sh/uv
- secondary ruff — "10–100× faster" linting (vendor claim) — github.com/astral-sh/ruff
- primary tokio 1.52.3 on crates.io — crates.io/crates/tokio
- primary wasm-pack 0.15.0 — github.com/wasm-bindgen/wasm-pack/releases
- primary wasm-bindgen 0.2.125 — github.com/wasm-bindgen/wasm-bindgen/releases
