The 20% that matters
Not a learn-Rust course. The specific slice of Rust that lets a Python AI developer vet the code AI writes and move the 10% of their pipeline that hurts.
You already ship AI features in Python. You're not curious about Rust as a hobby. You keep hitting the same wall: a pipeline that's too slow, an inference cost you can't tune away, an agent tool that should be one fast binary instead of a fragile script, or AI-generated code touching memory and concurrency that you can't fully vet.
You don't need all of Rust to fix that. You need about 20% of it.
This guide is that 20%: the slice that delivers most of the value for an AI developer, and nothing else. No lifetimes deep-dive, no macros, no unsafe. Two outcomes:
Rust is the language to let AI write for you, because the compiler is a verifier. In Python a bad AI suggestion runs fine until it doesn't; in Rust most of it never compiles. That makes Rust plus AI the most reliable pairing going, up to a point. The compiler still won't catch a fn that compiles yet is quietly wrong about ownership, blocks an async runtime, or .unwrap()s on a path that fails in production. It catches what AI gets wrong before it ships; you catch what compiles but shouldn't. This guide makes you that reader, fast, and in 2026 that reader is worth a lot more than someone who can only prompt.
Every value has exactly one owner; you either move it or borrow it (&). No garbage collector, no hidden copies.
Why you care: this is where AI-generated Rust is most often subtly wrong, and where your Python instincts mislead you. In Python you pass a list around and share it freely. In a Rust data pipeline, the difference between &data and data.clone() is the difference between zero-copy over a few GB and doubling your memory.
// Borrow: the caller keeps ownership, no copy. What you want in a hot path.
fn total_tokens(docs: &[String]) -> usize {
docs.iter().map(|d| d.split_whitespace().count()).sum()
}
// Smell: `docs: Vec<String>` by value would force the caller to give up
// (or clone) the whole corpus for a read-only count..clone() in a loop, you'll notice.Fallible functions return Result<T, E>. The ? operator propagates the error up. There are no unchecked exceptions.
Why you care: an agent or tool that calls a model, a network, a file. Every one of those fails. Rust won't let you forget a failure path; the type system makes it visible.
fn load_prompt(path: &str) -> Result<String, std::io::Error> {
let raw = std::fs::read_to_string(path)?; // ? returns early on error
Ok(raw.trim().to_string())
}.unwrap() / .expect() sprinkled through AI-generated code. Fine in a throwaway; a time bomb in a tool you ship.An enum is a type that is exactly one of a set of variants, each able to carry data. match forces you to handle every one.
Why you care: the single best tool for modeling an agent. A tool call is pending, or succeeded with a value, or failed with a reason, never two at once. In Python that's a dict you hope is shaped right. In Rust it's unrepresentable to get wrong.
enum ToolResult {
Ok(String),
Timeout,
Refused { reason: String },
}
fn handle(r: ToolResult) -> String {
match r {
ToolResult::Ok(v) => v,
ToolResult::Timeout => "retry".into(),
ToolResult::Refused { reason } => format!("blocked: {reason}"),
// add a 4th variant later and the compiler makes you handle it here.
}
}A trait is an interface. Types implement traits; functions can accept "anything that implements X."
Why you care: you don't need to design elaborate trait hierarchies. You need enough to read the AI infra crates you'll lean on (tokenizers, candle, Qdrant's client) because they expose behavior through traits. Recognizing impl Trait and dyn Trait is 80% of what you need day one.
trait Embedder {
fn embed(&self, text: &str) -> Vec<f32>;
}
// A function can now accept any backend without caring which one.
fn embed_all(model: &impl Embedder, docs: &[String]) -> Vec<Vec<f32>> {
docs.iter().map(|d| model.embed(d)).collect()
}async/.await with the tokio runtime lets you run thousands of I/O tasks concurrently on a few threads.
Why you care: fan-out. Ten tool calls or a batch of LLM requests that should run concurrently. In Python you're negotiating with the GIL and asyncio footguns; in Rust concurrent I/O is the default idiom.
use futures::future::join_all;
async fn fan_out(prompts: Vec<String>) -> Vec<String> {
let calls = prompts.into_iter().map(|p| call_model(p));
join_all(calls).await // all in flight at once
}std::thread::sleep) inside an async fn. It stalls the whole runtime.Write a function in Rust, expose it to Python with PyO3 + maturin, import it like any module. You do not rewrite your app.
Why you care: this is the PyO3 ROI in one motion: keep 90% of your Python, move the 10% that hurts. The CPU-bound loop that dominates your latency becomes a native function your existing code calls unchanged.
Worked example: RAG chunking, a real hot loop. The Python version, fine until your corpus is large:
def chunk(text: str, size: int, overlap: int) -> list[str]:
words = text.split()
step = size - overlap
return [" ".join(words[i:i+size]) for i in range(0, len(words), step)]The Rust version, exposed to Python:
use pyo3::prelude::*;
#[pyfunction]
fn chunk(text: &str, size: usize, overlap: usize) -> Vec<String> {
let words: Vec<&str> = text.split_whitespace().collect();
let step = size.saturating_sub(overlap).max(1);
(0..words.len())
.step_by(step)
.map(|i| words[i..(i + size).min(words.len())].join(" "))
.collect()
}
#[pymodule]
fn fastchunk(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(chunk, m)?)?;
Ok(())
}# after `maturin develop`
from fastchunk import chunk
chunks = chunk(doc, size=256, overlap=32) # same call site, faster loopNothing else in your pipeline changes. That's the whole pitch: Python where it shines, Rust where it hurts.
clap for args, cargo build --release, and you have one dependency-free binary that starts instantly.
Why you care: your agent tools and MCP servers want to be fast, portable, and un-babysittable: no venv, no cold start. That's Rust's home turf.
use clap::Parser;
#[derive(Parser)]
struct Args {
/// text to embed
input: String,
}
fn main() {
let args = Args::parse();
println!("{} tokens", args.input.split_whitespace().count());
}You now have enough to be the reviewer. When AI hands you Rust, scan for these:
_ => where you wanted every case handled: silent gaps when the enum grows (§3).'a to make an error vanish): usually the design is wrong, not the annotations.Catching these is the judgment the model can't give you. It's also, not coincidentally, most of what separates a senior engineer from a prompt.
This is the 20%: the map. Turning a map into muscle means doing it on your code: your slow endpoint, your agent, your pipeline. That's the work the cohort is built around: six weeks, your project, Bob & Jim in the room when the borrow checker wins.