Dyer InnovationStart a project

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.

Reason 1 · Compiled
No interpreter loop
Rust → machine code ahead of time. CPython re-dispatches every bytecode op, every iteration.
Reason 2 · No GIL
All cores, for real
Rust threads run truly parallel. Default CPython serialises bytecode on one lock.
Reason 3 · No GC
Freed at compile time
Ownership decides where memory dies while compiling. No refcounts, no pauses.
Reason 4 · Zero-cost
Abstractions vanish
Iterators, generics and traits compile away. A PyObject header never does.
CPU-bound loops ▸ Rust Multi-core ▸ Rust Latency tails ▸ Rust Single binaries / WASM ▸ Rust I/O-bound services ▸ Python Glue & scripts ▸ Python NumPy-shaped math ▸ Python Time-to-first-version ▸ Python

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

build time (once) paid per iteration native CPU
Python
.py source▸ bytecode▸ eval loop▸ unbox PyObject▸ type dispatch▸ refcount ±▸ CPU

▲ the four amber stages repeat every single iteration — that's the interpreter tax.

Rust
.rs source▸ rustc + LLVM▸ optimised machine code▸ CPU

▲ everything grey happens once, at cargo build. The loop itself is bare instructions on raw i64s.

CPython walks bytecode in an evaluation loop; every value is a heap-allocated 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

core 0
core 1
core 2
core 3

Four threads take turns holding one lock. Wall-clock ≈ one core's worth. Grey = paid-for silicon idling.

Rust — no GIL

core 0
core 1
core 2
core 3

Threads run at once; the compiler statically rejects the data races. ≈4× the work, same wall-clock.

Honest caveat: free-threaded CPython is officially supported, no longer experimental, as of Python 3.14 (PEP 779) — but still opt-in, default builds ship the GIL, and it costs single-threaded performance and memory. This gap is closing over years, not gone today. Source: PEP 779; free-threading HOWTO.

Diagram C — When does memory get freed?

Python — refcount + cycle GC
obj▸rc=2▸rc=0▸freed

Every assignment bumps a counter at runtime. Cycles need a collector pass that can stop the world.

cost: runtime, always

Rust — ownership (move)
owner a▸a moved▸owner b▸b scope ends → freed

One owner. The compiler knows the line the owner dies on, and frees there.

cost: compile time, once

Rust — borrowing
owner▸&shared&shared&mut ✗

Many readers or one writer, never both. That one rule makes Diagram B provably safe.

cost: compile time, once

Rust — stack by default
i64 on stack▸pop

A number is a number. In Python it's a heap object with header, type pointer and refcount.

cost: ~free

The whole trick: Rust moves "when is this safe to free?" from runtime (where a GC pays for it) to compile time (where the borrow checker pays for it, once, in your patience). Source: the Rust Book, ch. 4.

Chart D — Measured gap on CPU-bound work (lower is better)

Rust CPython 3
n-body — Benchmarks Game, fastest entry
Rust
2.19 s
CPython 3
≈360 s · ~164×
binary-trees — Benchmarks Game, fastest entry
Rust
1.07 s
CPython 3
33.37 s · ~31×
Energy for the same task suite — Pereira et al. (normalised)
Rust
≈58 J
Python
up to ≈4,604 J
Rust-backed Python tooling — vendor-claimed speedups
uv vs pip
10–100× (vendor)
ruff vs flake8
"10s–100s ×" faster (vendor)
Read these honestly. Benchmarks Game entries are hand-optimised, CPU-bound micro-programs — best case for Rust, worst case for CPython. A ceiling, not an expectation: real hot-function speedups are more like 5–50×, and PyPy narrows several of these. uv/ruff figures are vendor-claimed. Bars scale per-group, not across. Sources: Computer Language Benchmarks Game; Pereira et al. (SLE'17); Astral READMEs.

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.

Variables & mutability easy · immutable by default
Python
x = 5
x = 6          # always rebindable
CONST = 3.14   # convention only

Nothing stops any reassignment.

Rust
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.

Types easy · static, but inferred
Python
def add(a: int, b: int) -> int:
    return a + b
# hints are optional, unenforced
add("a", "b")  # runs fine!

Type errors surface in production.

Rust
fn add(a: i64, b: i64) -> i64 {
    a + b        // no `return`, no `;`
}
let n = 5;       // inferred i32
// add("a", "b") // ✗ won't compile

Like mypy, but mandatory and never wrong.

Ownership & borrowing ★ the hump · no Python equivalent
Python — everything is shared
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.

Rust — one owner, or borrows
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 ends

Lifetimes (&'a T) just prove a borrow outlives nothing it shouldn't.

Errors medium · values, not control flow
Python — exceptions
try:
    v = int(s)
except ValueError:
    v = 0
# nothing forces you to catch it

Invisible in the signature; escapes at runtime.

Rust — Result / Option
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 errors

Failure is in the type; the compiler makes you handle it.

Structs, enums, traits medium · composition over inheritance
Python — classes + duck typing
@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.

Rust — struct + trait
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 type

Traits = duck typing, compile-checked. No inheritance.

Collections easy · same shapes, different names
Python
xs = [1, 2, 3]          # list
d  = {"a": 1}           # dict
s  = {1, 2}             # set
t  = (1, "a")           # tuple
txt = "hi"              # str
Rust
let 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 &str

One genuine wrinkle: owned String vs borrowed &str.

Iterators & closures easy · you already know this
Python
evens = [x * 2 for x in xs if x % 2 == 0]
total  = sum(xs)
f = lambda a: a + 1
Rust
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.

Packaging easy · cargo is pip+venv+pytest+black in one
Python
python -m venv .venv
pip install requests
# pyproject.toml / requirements.txt
pytest ; black . ; ruff check .

Several tools, several configs.

Rust
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 medium · same syntax, different engine
Python — asyncio
async def fetch(u):
    async with session.get(u) as r:
        return await r.text()

asyncio.run(fetch(url))

Cooperative, single-threaded, GIL-bound.

Rust — tokio 1.52
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.

Table 1 — Difficulty map for a Python developer
ConceptPython analogueEffort
Iterators, closures, collectionsComprehensions, lambdas, list/dict/setTrivial — a rename
cargo / crates.iopip + venv + pyprojectTrivial — strictly nicer
Static typesType hints, enforcedEasy if you use mypy
Result / OptionExceptions, NoneA real re-think
Traits & enumsProtocols / ABCs; no sum typeA real re-think
asyncasyncio (same keywords)Syntax easy, ecosystem fiddly
Ownership · borrowing · lifetimesNone. 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.

1
Install the toolchain — 5 min
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh — brings rustc, cargo, clippy, fmt. New stable every 6 weeks via rustup update.
2
First program — 5 min
cargo new hello && cd hello && cargo run. Add rust-analyzer to your editor — inline errors make the borrow checker teachable, not cryptic.
3
The Book, chapters 1–6 — week 1
Free, official, the best language book in the industry. Ch. 4 (ownership) is the one that matters — read it twice.
4
★ Fight the borrow checker — weeks 2–5
You'll write Python-shaped Rust and it won't compile. That's the compiler catching bugs you'd have shipped — not pedantry. Escape hatches while learning: .clone() freely, use Vec not references, ignore lifetimes until forced. Make it compile now; optimise later.
5
Rustlings — weeks 2–4, in parallel
cargo install rustlings → rustlings init → rustlings. ~100 small broken programs you fix, watched and re-run. Where ownership stops being theory.
6
Ship something real — week 5+
Not a toy. Port one hot Python function via PyO3 (§4) — a working artefact, small enough that the borrow checker has only a few lines to argue with.

Chart E — Expected productivity, honestly (qualitative)

Week 1
Weeks 2–3
Weeks 4–6
Months 2–3
Month 6+

▸ relative to your Python speed on the same task

Illustrative, not measured — but the weeks 2–3 dip is real and near-universal, and knowing it's coming is the difference between pushing through and quitting. The curve turns up once ownership clicks, then keeps climbing: the compiler is doing your debugging.

Your first week:

4. How You'd Actually Use It

Four on-ramps. Only one is the right first move.

★ On-ramp A · start here
PyO3 + maturin
Keep every Python service as-is. Move one hot function to Rust, import it. Reversible in an afternoon.
On-ramp B
Standalone CLI / service
Single static binary, no interpreter to ship. Good for sidecars and latency-sensitive daemons.
On-ramp C
Rust → WASM
wasm-pack + wasm-bindgen → wasm32-unknown-unknown. Native-ish compute in the browser, callable from JS.
On-ramp D
Just use Rust-backed tools
You already benefit: uv, ruff, Polars, Pydantic v2 core are Rust under a Python skin. Zero cost.

Workflow — the whole PyO3 loop, end to end

1 · scaffold & build (maturin 1.14)
pip install maturin
maturin new -b pyo3 fastbits
cd fastbits
maturin develop --release
# builds the Rust ext + installs it
# into your active venv
2 · src/lib.rs (PyO3 0.29)
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(())
}
3 · use it from Python — unchanged callers
import fastbits

fastbits.sum_sq([1, 2, 3])   # → 14
# same signature as the Python
# version it replaced. Callers
# never know.
4 · ship it
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.

The entire integration surface. No FFI headers, no 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."

Table 2 — Candidate shapes, not commitments
CandidateWhy it might qualifyVerdict
A compute-hot pipeline stagePure-Python loops over many items (scoring, parsing) rather than NumPy or a DB.PyO3 candidate — profile it
An audio / real-time latency pathPer-frame DSP or VAD with a hard latency tail: where GC pauses and the GIL bite.PyO3 or sidecar — check p99
Any FastAPI endpointWaiting on Postgres, an LLM API, the network.No — I/O-bound
Agent orchestration / glueDominated by the calls it makes; iteration speed is the asset.No — keep Python
Already NumPy / Polars / CUDAThe 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.

Have you profiled it and found a Python-executed hot loop?
No → stop. Profile first.
▼ yes, I have
Is it I/O-bound, or already NumPy/Polars/C/CUDA?
Yes → Rust won't help. Keep Python.
▼ no, it's real Python CPU work
Would a library (Polars, uv, ruff, orjson) already solve it?
Yes → use it. Someone wrote the Rust for you.
▼ no, it's your own logic
Do you need a standalone binary, browser WASM, or no-Python deployment?
Yes → full Rust crate (on-ramp B / C)
▼ otherwise
Default — keep the Python service, extract the one function
PyO3 + maturin

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

Python interop — PyO3, maturin

Python runtime — the GIL

Benchmarks & energy

Rust-backed Python tooling & ecosystem

How-To Guides · Published 17 July 2026

Built from public sources. Plans, prices and versions change: check the linked sources before you rely on them.

Get new guides by emailOne short note when a new How-To Guide lands. Free.

Get new guides by emailBrowse all guides