# Rust for Python Developers: A Visual Guide to Talk Python Episode 563

**Guide**: https://talkpython.fm/guides/563/getting-started-with-rust-as-python-devs
**Episode**: [Talk Python to Me #563: Getting Started with Rust as Python Devs](https://talkpython.fm/episodes/show/563/getting-started-with-rust-as-python-devs)
**Featuring**: Christopher Trudeau (co-host of The Real Python Podcast)
**Published**: October 07, 2026

**TL;DR:** Rust is already underneath a lot of the Python you run: ruff, uv, Pydantic, Polars and Granian. You don't need to write Rust to benefit, but knowing enough to read it pays off. The syntax looks like typed Python and cargo feels a lot like uv. What's genuinely new is the memory model Python hides from you, and the advice for using Rust from Python is to measure before you rewrite anything.

A few years ago, Michael Kennedy, host of Talk Python, had a running joke: "My joke used to be that if you switch to Rust, you're probably going to get VC funding." That was around the time Astral and Pydantic took off. Christopher Trudeau, co-host of The Real Python Podcast, had an update ready: "Now it needs Rust plus AI, but I think that's still true." Funny, but there's something to it. Rust has become the go-to way to make Python faster, and it's spreading well beyond Python, too. That very morning, Christopher had seen news that one of the major CPU makers is moving a lot of its device drivers to Rust.

So should a Python developer learn Rust? Michael's angle is practical. He's not trying to become a Rust expert. He wants to know enough that poking around inside Pydantic, ty or some other extension makes sense, and maybe write a little of it to speed up a hot spot in his own code. Christopher has been getting into Rust in earnest and wrote Talk Python's Up and Running with Rust course, and that course teaches it by comparison to the Python people already know.

This guide takes the same route. It starts with where Rust already shows up in Python, then works through how the two languages differ: compiled code, the stack and the heap, hardware-sized integers, ownership and the borrow checker. After that come the tools (rustup, cargo and crates.io), the bridge back to Python through PyO3 and maturin, and when it's actually worth reaching for Rust at all.

## How fast is ruff? Linting all of CPython in 0.3 seconds

![0.3 seconds, in giant glowing orange letters, for linting the entire CPython codebase from scratch. Below it: Ruff didn't skip anything. It's just written in Rust. Orders of magnitude change how you work.](https://blobs.talkpython.fm/guides/563/02.webp?cache_id=941627e4)

Why did Rust tooling take over the Python world so quickly? Christopher Trudeau's explanation starts with the tools it replaced. Most of the tools Python developers relied on for years were themselves written in Python, and they were slow. And most of the time, nobody minded. You kicked off the job, went for another cup of coffee, and it was done when you got back.

The catch is that a little more speed doesn't change anything. Make a tool 10% faster and nobody notices. Make it several zeros faster and you start working differently. A couple of key libraries showed that early on, everyone went, wait a second, there's something here, and a whole wave of Rust-backed Python tools followed.

Michael Kennedy's moment came with ruff. He ran it over the Talk Python Training app, which is bigger than it looks from the outside: about 170,000 lines of Python, "not huge, but it's not a toy." Ruff reported four errors and was finished almost before he hit enter. His first reaction was disbelief. How could it be finished? Had he forgotten to tell it to go into the subdirectories? Nope. It had parsed the whole thing and checked it against around a hundred rules.

For a number anyone can compare against, Michael pointed to linting the entire CPython code base from scratch: 0.3 seconds, or about three blinks of an eye. That's the number in the image above. The 170,000 lines belong to the Talk Python Training app, not to CPython.

Christopher sees an even bigger gap. He's slow to upgrade his machines and usually runs a couple of processor generations behind. When you're linting a couple hundred thousand lines of code on a slow machine to begin with, the jump to ruff makes a huge difference.

Does it matter that ruff is written in Rust? For this kind of tool, not really, and both Michael and Christopher said as much. The Python grammar is public, and parser generators exist for many languages, so someone could have built a fast Python linter in C or anything else. Rust just happens to be the language that's become the way to do this kind of work. That's also why this category is the easiest to forget about: you use ruff without ever seeing a line of Rust.

> "Your world shifts when you start seeing, you know, orders of magnitude, right? Like 10% faster, you're not going to notice it when you're starting to talk multiple zeros times faster, this changes things."
>
> Christopher Trudeau

**Resources**

- [Ruff documentation](https://docs.astral.sh/ruff/)
- [Ruff on GitHub](https://github.com/astral-sh/ruff)
- [CPython on GitHub](https://github.com/python/cpython)
- [Astral](https://astral.sh)

## Three ways Rust shows up in Python: tools, libraries and servers

![Rust is already in your stack. You just choose how to interact with it: as external tooling, optimized internal modules, or a host environment for your runtime. Model 1, Adjacent (Tools): Ruff, uv. Model 2, Internal (Libraries): Pydantic, Polars. Model 3, External (Servers): Granian.](https://blobs.talkpython.fm/guides/563/03.webp?cache_id=267a5b22)

Rust in Python can mean a few very different things. Christopher Trudeau breaks it into three modes, and once you've seen them, it's much easier to tell what's going on in any given project.

**Tools that happen to be Rust.** This first mode has almost nothing to do with Python. Linters, formatters and package managers like ruff and uv are third-party tools you run to make your Python better. They don't plug into the interpreter at all. They're written in Rust because Rust is fast (the previous section covers how much that matters), and that's about it.

**Libraries Python imports.** The second mode, and the one Christopher thinks is the most common, is a library written in Rust and exposed to Python as an ordinary module. Pydantic and Polars work this way. The Rust does the heavy lifting, a thin veneer on top makes it importable, and inside your Python code you treat it like any other module. You don't have to know or care how it was built. This is where Python gets to borrow Rust's speed for heavy crunching, and it's where Christopher thinks Rust is really starting to shine.

**Rust programs that run Python.** The third mode flips the relationship. A Rust program can embed the Python interpreter, which lets it use Python as a scripting or configuration language inside a bigger system. Granian is the example here: a web server built in Rust that runs your Python web app inside it.

Michael Kennedy came up with an analogy for keeping these straight. It involves caramel. Pydantic is a caramel-dipped apple, with Python on the outside and a Rusty core. Granian is more like a caramel M&M, with a hard Rust shell protecting the Python in the middle. For the tools, Christopher came up with the better picture: slices of apple next to a bowl of caramel, separate things you can mix and match as you like. By that point the analogy was officially stretched, and there was caramel everywhere.

Why does any of this matter if you never plan to write Rust? Because the second and third modes are code you're running every day. As Michael put it near the end of the conversation, even if you don't want to write Rust, a lot of really important libraries are backed by it, so you want to be able to read them and maybe contribute to them. The sections that follow are about getting to that point.

> "So there's kind of three ways that Rust intersects with the Python world. And one has almost nothing to do with Python."
>
> Christopher Trudeau

**Resources**

- [Ruff](https://docs.astral.sh/ruff/)
- [uv](https://github.com/astral-sh/uv)
- [Pydantic](https://docs.pydantic.dev)
- [Polars](https://pola.rs)
- [Granian](https://github.com/emmett-framework/granian)
- [ty](https://github.com/astral-sh/ty)

## Compiled vs interpreted: Rust machine code vs Python bytecode

![The Physics of Two Languages. Python (The Stratosphere) vs Rust (The Engine Room). Execution: bytecode plus interpreter vs native machine code. Memory: garbage collected on the heap vs strict ownership and a stack/heap split. Sizing: boundless integers vs hardware-specific i8, i32, u8. Errors: discovered at runtime vs compile-time guarantees.](https://blobs.talkpython.fm/guides/563/04.webp?cache_id=ea175c1f)

The table above is a decent map of where Python and Rust part ways. Three of its rows get their own sections later in this guide: memory in [the stack and the heap](#stack-vs-heap-why-everything-in-python-is-a-pointer) and [ownership](#rust-ownership-explained-for-python-developers), sizing in [integer types](#integer-types-in-rust-vs-python-i8-u8-and-boundless-ints), and errors in [the borrow checker](#the-borrow-checker-and-rusts-compiler-error-messages). This section takes the first row, since it's the one that trips people up most.

Wait, doesn't Python compile too? It does, and according to Christopher Trudeau, that's exactly where his students get tangled up. Python compiles your source into bytecode, a binary file that isn't specific to any platform. His analogy is a formula in an Excel file. The file stores the formula in binary, but Excel is the thing that actually runs it. In the same way, your Python interpreter is specific to your machine, but the bytecode isn't. Hand someone your bytecode and their interpreter takes care of the rest.

Rust goes all the way down to machine code for one specific platform, and you get a tight binary file out the other end. If Christopher hands you his binary and you're on the same kind of system, it runs. If you're not, it doesn't.

That's a real trade. Distribution gets way easier: here's the executable, go nuts, you're done (plus any dynamically loaded libraries it needs). But it's platform specific, so supporting several platforms means compiling for each of them. It's also why a Python package with Rust inside ships as a set of platform-specific wheels rather than a single source-only one.

Michael Kennedy admitted he's jealous of the easy half. He'd love a `python --build` command that turns out one file he could put on a server, hand to a person or drop into a Docker container, without a pile of supporting pieces around it.

The other casualty is the REPL, something Python developers take for granted. Rust has to compile, so a true REPL isn't really possible. There are hacks that fake one, and Christopher finds them kind of hilarious: behind the scenes they keep a whole program around and recompile it every single time you enter a line. Michael pictured the invisible `fn main` and curly braces wrapped around each line before a cargo build and run, and Christopher confirmed that's exactly what's going on. He played with these tools a few times, found them painful even with Rust's speed, and went back to keeping a scratch `foo.rs` file instead.

> "Just a thing that I could put on a server, I could give to a person, I could put in Docker, and it doesn't have to have a bunch of other stuff there to make it possible."
>
> Michael Kennedy

**Resources**

- [Python glossary (bytecode)](https://docs.python.org/3/glossary.html)
- [Hello, World! in the Rust book](https://doc.rust-lang.org/book/ch01-02-hello-world.html)
- [PEP 427: the wheel format](https://peps.python.org/pep-0427/)

## Stack vs heap: why everything in Python is a pointer

![Hardware Alignment and The Heap. Python (The Stratosphere): a row of slots points to values scattered across memory, with a callout that everything is a pointer. Rust (The Engine Room): values sit side by side in the CPU cache. Python hides the hardware behind indirection. Rust forces you to map directly to it.](https://blobs.talkpython.fm/guides/563/05.webp?cache_id=26e3e30d)

You can start writing Rust without fully understanding the stack and the heap, Christopher Trudeau says, but you won't get very far. So here's the short version.

The stack is where function calls happen. When code jumps into a function, the address to come back to goes onto the stack, along with the function's parameters. Together they make a stack frame. When the function returns, its frame pops off, and the program carries on from the address that's now on top. The heap is the general space you allocate from. Longer-lived values, and things you pass around between functions, tend to be created there.

None of this is new with Rust. It's how compiled languages, and the machines under them, have always worked. Rust just puts more of it in front of you. In Python you only think about the stack when something fails and you get a stack trace. In Rust, once you start passing objects around, two questions become very important: is this on the stack or the heap, and who owns it? The [ownership](#rust-ownership-explained-for-python-developers) section picks that up.

Michael Kennedy pointed out the irony here. Python never makes you think about pointers, while in C, C# and Rust you talk about stacks, heaps and dereferencing all the time. Yet Python is the most pointer-heavy of the lot, because literally everything lives on the heap. Even the number one is a pointer to an object on the heap. Christopher's take is in the quote below: when everything is a pointer, there's nothing to tell apart.

CPython even has an optimization that leans on this. Small integers are created ahead of time, so every time you write a literal like 72, it points at the same immutable object instead of making a new one. Michael remembered the range as roughly negative five to 255. In current CPython it runs from -5 through 256, so Christopher's joke about having three copies of 254 but only one copy of two needs a slightly bigger number to land. Pick 300 and you really can end up with separate objects holding the same value. Michael called the whole thing funky, and Christopher's advice was not to think about it too much.

There's a performance angle, too. Got an array of Python numbers? Where do they land in memory? All over the heap. CPython has arenas and blocks that try to keep things closer together, Michael noted, but the numbers are still out there somewhere. An array of 10 numbers in Rust is contiguous in memory, which is exactly what the CPU cache is built to work with.

> "Well, this is how Python got away with not having pointers. It was everything's a pointer, so we don't have to differentiate."
>
> Christopher Trudeau

**Resources**

- [What is ownership? (the Rust book)](https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html)
- [CPython on GitHub](https://github.com/python/cpython)

## Integer types in Rust vs Python: i8, u8 and boundless ints

![The Boundaries of the CPU. Python: a Boundless Integer; you are never going to get a Python integer into a CPU register, and limitless integers need hidden machinery to grow. Rust: crates labeled i8 (signed), u8 (unsigned 0-255) and f64. Rust removes the machinery and makes you choose your bit width up front.](https://blobs.talkpython.fm/guides/563/06.webp?cache_id=3374ef68)

Python integers have no upper limit. For certain kinds of math, that's fantastic. Christopher Trudeau had recently read about someone building a SIMD-style machine out of bitwise operations on thousand-byte integers, and Python just shrugs and says, sure, that's a really big integer, no problem.

Rust sits closer to the machine, so it uses integers the way your CPU does. Pick an 8-bit integer and you can't put nine bits in it. The compiler catches that most of the time, depending on how you got there. Still, you're now making a design decision up front: how many bits does this value get? Usually that's no problem. Every so often it means hitting a boundary you'd never have run into in Python.

You also have to decide whether a number is signed. A single unsigned byte (`u8`) holds 0 to 255. If you want negative numbers in the same space, the CPU has to read those same bits differently, and the range gets split across negative and positive values. For Rust's `i8`, that's -128 to 127. The price is that you can only go about half as high.

Python has these concepts too, Christopher pointed out. They're just buried further down. Packing bytes into the perfect network packet? Signed and unsigned sizes matter a lot. Doing math at the command line? You'll never see them. Rust, like most compiled languages, doesn't give you that choice.

So why would anyone give up boundless integers? Speed, of course. Python's limitless integer needs machinery inside it to handle the moment a value outgrows its current chunk of memory, and that machinery is overhead on every number. Rust doesn't have it, so its integers map exactly onto what the CPU does. Michael Kennedy put it bluntly: "You're never going to get a Python integer into a CPU register."

Going the other direction was its own kind of shock. Michael spent about 15 years in C++ and C#, languages that care a lot about the shape and size of numbers, before he picked up Python. At first he was sure he was doing something wrong. Where do you tell it how big the number is? Is it 64-bit? Then it clicked. Wait, it just grows? That's crazy. Christopher had a gentler landing. He started with Python as scripting glue, writing 50 or 100 line scripts to solve small problems, where nobody cares about integer sizes. His point is that how much this matters depends on where you're coming from and what you're using the tool for.

> "So that beauty of Python's limitless integer means there has to be machinery inside of that to handle when you make it bigger than what the current chunk of memory is. And Rust doesn't have that machinery."
>
> Christopher Trudeau

**Resources**

- [Data types (the Rust book)](https://doc.rust-lang.org/book/ch03-02-data-types.html)
- [Arbitrary-precision arithmetic](https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic)

## Rust ownership explained for Python developers

![Rule number 1: Only one thing can own a value. On the Python side, a runner hands copies of a baton to three others: pass a value, GC cleans up later. On the Rust side, one runner hands the single baton to the next: ownership transferred. Pass it around freely in Python and the garbage collector eventually cleans up. Pass it in Rust, and it won't compile.](https://blobs.talkpython.fm/guides/563/07.webp?cache_id=7cb88b55)

What trips up people coming to Rust from Python? For Christopher Trudeau, it's memory management. Python takes care of it so thoroughly that plenty of Python developers have never had to think about it. Then Rust asks them to, and the first reaction is: I have to do what? And why do I have to do that?

The core rule fits in one sentence. Rust has objects, but every object belongs to somebody, and only one thing can own a value at a time. In Python you pass values around willy-nilly, as Christopher put it, and the garbage collector cleans up whatever's left over. Carry that habit into Rust and it often turns into a compile error. Pass a value into a function, and that function now owns it. Try to use it again afterward and the compiler stops you, as the image above shows with the baton changing hands.

Is that a feature or a nuisance? Both, and Christopher is upfront about it. On one hand, it's the beauty of the language, because it makes sure certain kinds of bugs never happen. On the other hand, you can't play as loosely as you can in a dynamic language. You'll be thinking, I'll just pass this around, and then: wait, I can't. Hence the joke in the quote below.

For a newer developer, that can be very frustrating. The threshold for getting started is higher, Christopher says, because you're constantly fighting the compiler before you get to play with an idea. Better to know that going in.

Ownership also ties straight back to the [stack and the heap](#stack-vs-heap-why-everything-in-python-is-a-pointer). Once you start passing objects between functions, knowing where a value lives and who owns it is how Rust knows when to free memory without a garbage collector. Python hides all of this from you. Rust makes you answer for it at compile time, and then the compiler checks your answers.

And yes, the borrow checker, the part of the compiler that enforces all this, showed up early. Michael Kennedy marked the moment for the record: six minutes, 57 seconds into the recording. The next section is about what fighting it is actually like, and what you get for your trouble.

> "There's a joke in the Rust world that there are no bugs. All your time isn't debugging. All your time is getting it to compile."
>
> Christopher Trudeau

**Resources**

- [Understanding ownership (the Rust book)](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html)
- [The Rust Programming Language](https://doc.rust-lang.org/book/)

## The borrow checker and Rust's compiler error messages

![The Trial of the Borrow Checker. An armored guard stands in front of a vault beside compiler output: error E0382, borrow of moved value name, with help: consider cloning the value. You will fight the compiler, but its error messages are explicitly helpful and correct 90% of the time. The reward: a whole category of memory bugs becomes impossible to write.](https://blobs.talkpython.fm/guides/563/08.webp?cache_id=3ddae8e6)

If you're going to spend a lot of time fighting the compiler, it had better be a good sparring partner. According to Christopher Trudeau, Rust's is. He calls its error messages absolutely fantastic. They're very explicit, and they often include a suggestion along the lines of "maybe you need to do this." In his experience, those suggestions were right 80 or 90% of the time.

The image above shows a typical one. A `String` gets passed into a function, which takes ownership of it, and then the code tries to print it again. The compiler points at the line where the value moved, the line where it was used after the move, and offers a fix: clone the value, if the performance cost is acceptable.

There's a vocabulary to pick up first. Once you know how the messages talk, an error stops being a mystery and becomes: oh, okay, that's kicking off the borrow checker, I'll go fix it. Christopher admits his bar might be low. He remembers C compilers reporting an error three lines after the code that actually caused it, so you had to hunt around the general area to find what went wrong.

So what do you get for all that up-front work? To answer that, Christopher went back to how C stores a piece of text. Store "Talk Python" and you get those bytes followed by a null byte, and that null is the only way the program knows where the string ends. Write something longer than the space you set aside and there's no null, so the program keeps walking into the next chunk of memory. Free a block of memory but keep a pointer into it, and the computer will happily read whatever is there now. With two strings next to each other, you might just get a wrong message on screen. With something trickier after it, your program falls over. That's what makes this family of bugs so nasty. They only crash sometimes.

Rust isn't bug free, and Christopher was careful to say so. But it won't let you create that whole category of bug. Types get the same treatment. In Python without a type checker, a type mismatch is something you find at runtime. In Rust, it's caught before the program ever exists.

Michael Kennedy connected this to learning C++ in the 90s from a book the size of the Yellow Pages. When his code finally compiled, he thought he was done. Nope. That's where the real debugging and problem solving started. His conclusion: "compiling does mean more in Rust than it used to in other languages."

> "So Rust isn't bug free, but there is a category of bugs that it just doesn't let you create."
>
> Christopher Trudeau

**Resources**

- [The Rust Programming Language](https://doc.rust-lang.org/book/)
- [Null-terminated string](https://en.wikipedia.org/wiki/Null-terminated_string)
- [ty type checker](https://docs.astral.sh/ty/)

## Rust syntax for Python developers: fn, braces and implicit returns

![If you read typed Python, you can read Rust. The same process_data function side by side in Python and Rust, with callouts for familiar types, identical returns and the same loops, and a warning on the Rust version's final false: implicit return, no keyword and no semicolon at the end of the block. Swap def for fn and embrace the brace brackets.](https://blobs.talkpython.fm/guides/563/09.webp?cache_id=898759d6)

Before getting into the differences, Christopher Trudeau suggests starting with what isn't different. There's more of it than you'd expect. Hand a Python developer some fairly clean Rust and they'll probably have an idea of what it does. The loops work the same way. `if` and `else` are similar, though there's no `elif`. You write `else if`, which suits Christopher fine, since elif always had him scratching his head. There are tuples and arrays, iterators and collections, and the same `for ... in` loop.

Type hints give you the biggest head start. Rust is strictly typed, so you have to say what everything is, and the declarations look a lot like typed Python: a variable name, a colon and a type, and an arrow for what a function returns. Michael Kennedy noted the same shape in TypeScript and Swift. Christopher's theory is that everyone ultimately borrowed from C, the granddaddy of most of the languages we use. One thing both of them needed time to get used to: the type goes after the name, where C would put it in front.

Then the differences show up. Some are cosmetic. Functions are declared with `fn` instead of `def`, and Christopher liked that as soon as he saw it, because it tells you exactly what the thing is.

Blocks are marked with brace brackets rather than indentation. After nearly two decades of Python, Christopher has come down firmly against making life easy for the parser (see the quote below). Michael gives Python's approach an 80% plus, 20% minus. The plus: indentation forces you to write code whose structure matches how it reads. The minus: he's had a reformat quietly change what his code did because one line was indented three spaces instead of four. With braces, the formatting can be a total mess, even minified, and one reformat puts it back together reliably. Christopher, a veteran of the tabs versus spaces war of the 90s, does find it interesting that Python settled that fight at the syntax level.

Here's one difference Christopher really likes. Almost everything in Rust is an expression. A block in braces can return a value, so you can assign the result of a whole block to a variable, a bit like an inline function. He finds that elegant.

What neither of them likes is where that leads next. The last expression in a function, with no semicolon, is its return value, like the bare `false` at the end of the Rust function in the image above. Christopher called it "this weird thing." Michael was more emphatic: he is "never ever going to be on team implicit return."

> "I think after having coded in Python for almost two decades, I think I've come down on the side of screw the parser."
>
> Christopher Trudeau

**Resources**

- [Rust by Example](https://doc.rust-lang.org/stable/rust-by-example/)
- [Functions (the Rust book)](https://doc.rust-lang.org/book/ch03-03-how-functions-work.html)
- [Python typing module](https://docs.python.org/3/library/typing.html)

## Installing Rust: rustup, cargo and Cargo.toml for Python developers

![Ecosystem Translation Matrix. pyenv maps to rustup (toolchain installer). uv and pip map to cargo (package and build manager). pyproject.toml maps to Cargo.toml (project manifest). PyPI and wheels map to crates.io and crates (package registry). The tooling maps perfectly. In fact, modern Python tools like uv were directly inspired by cargo.](https://blobs.talkpython.fm/guides/563/10.webp?cache_id=3e5b0b5c)

On Linux or macOS, installing Rust is a breeze. Go to rust-lang.org, copy the shell command from the front page and paste it into a terminal. Michael Kennedy called it the thing we normally do: run arbitrary code from the internet as root and assume it's fine.

That command installs rustup, which Christopher Trudeau compares to pyenv: it handles installing and updating the language itself. Rustup in turn installs `rustc`, the compiler, and, more importantly, cargo. Cargo is the tool you actually use, and it's very uv-like. You almost never call `rustc` yourself.

Here's the whole loop, from new project to release build:

```bash
cargo new hello          # Cargo.toml, src/main.rs, plus a git repo
cd hello
cargo run                # build and run the debug version
cargo run -q             # same, without cargo's build output
cargo build --release    # optimized build in target/release/
```

`cargo new hello` creates a `Cargo.toml`, the equivalent of `pyproject.toml`, a `src` directory and a stub main file with a working function in it, so you have a program that compiles right away. By default it also creates a git repository and a `.gitignore`, which drives Christopher nuts. He wants to control that himself, and `cargo new hello --vcs none` skips it. `cargo run` builds and runs the program, so you don't have to dig the executable out of the `target` folder. Debug builds are about 30% larger because they carry debug information; the release build is the one you'd hand to someone. Christopher's one remaining gripe is the two or three lines of build output cargo prints before your program's own output, which is what `-q` is for.

Rust also moves fast. A new version ships every six weeks, which sounds insane if you're coming from Python. (Michael found a fresh one waiting the morning of the recording.) How does anyone keep up? Editions, a separate idea on a three-year cycle. Code stays compatible within an edition, and the compiler can still build for older ones, so you set the edition target in `Cargo.toml` and old code keeps compiling. Christopher likes it and thinks Python might benefit from something similar, though he admits a forever promise of backward compatibility "smells of maintenance hell."

And the influence between the two worlds runs in a circle. Much of Python's recent packaging revolution, uv included, was inspired by cargo, Michael pointed out, but cargo was designed when PyPI and npm already existed, so its designers could see what went wrong. Christopher went further, with an opinion he said he wasn't trying to start a flame war over (see the quote below). Then he softened it: Rust's target is simpler, because compiled code sheds a lot of the complications Python's dynamism creates. Michael's example of that complexity was a Python package that also needs a Fortran compiler. Michael added that wheels were a big step forward, and Christopher agreed that Python packaging is making progress.

> "The decision that the PSF made that managing packages wasn't really part of the language and that we should have this plethora of third-party tools to do that, I think that was a mistake. And I think the Rust people recognized that and decided to bundle this in."
>
> Christopher Trudeau

**Resources**

- [Install Rust](https://www.rust-lang.org/tools/install)
- [rustup](https://rustup.rs)
- [The Cargo book](https://doc.rust-lang.org/cargo/)
- [The Rust edition guide](https://doc.rust-lang.org/edition-guide/)
- [uv documentation](https://docs.astral.sh/uv/)
- [pyenv](https://github.com/pyenv/pyenv)

## crates.io vs PyPI: Rust's small standard library and the trust problem

![The Trust Trade-off. A Python toolbox overflowing under a Batteries Included banner, beside a small Rust toolbox with a warning sign amid floating crates. Rust keeps its standard library tiny; even a random number requires a third-party crate. You must manage your supply chain and decide whom to trust.](https://blobs.talkpython.fm/guides/563/11.webp?cache_id=43ad6225)

Besides building your code, cargo manages crates, Rust's version of Python's wheels. They live on crates.io, Rust's answer to PyPI, where a fairly robust community writes and trades code.

How big is it? Michael Kennedy looked it up on the spot: 331,019 crates, against roughly 889,000 to 890,000 projects on PyPI. He didn't think the raw count was the core metric, though, and Christopher Trudeau agreed. If anything, he argued, the comparison is unfair in the other direction. Coming from Python's batteries-included world, you'll find there aren't many batteries in Rust. Something as simple as generating a random number is a crate. That's on purpose, to keep the footprint small, and Christopher gets why. But it means a lot of everyday code comes from outside the standard library.

And that raises a question Christopher cares about. He describes himself as paranoid about where his code comes from. In Python, sticking to the standard library gives you a degree of safety. Once he steps outside it, he wants to hear from someone who's used a package and trusts it, and for popular libraries there's enough written about them to get a feel. In Rust he hasn't yet figured out where that signal comes from. When even random numbers live in a crate, there's a bit more rolling the dice on whether to trust the code. He suspects some of that is just being new to the community.

Michael's reply: "just because you're paranoid doesn't mean they're not after you." Given the state of supply chain attacks, Christopher didn't argue: "In the world of the internet, they are out to get you."

Why is Python so different here? Michael has a theory. Python appeared around 1991, before PyPI, before the idea of a package manager, before the web itself. Getting code after the fact was really, really hard, so a big standard library made sense. If it had been easier, maybe the choices would have been different. Christopher took that in a generous direction: we should all be so lucky as to have our early design decisions questioned because people are still using our software 35 years later.

> "If I'm in the Python world, I trust the standard library. If I stick to the standard library, I know I have a degree of safety."
>
> Christopher Trudeau

**Resources**

- [PyPI](https://pypi.org)
- [Python's random module](https://docs.python.org/3/library/random.html)

## PyO3 and maturin: calling Rust from Python

![The Bridge: PyO3 and Maturin. A Rust gear tagged with the pyfunction macro feeds through a funnel labeled Maturin and PyO3 into import my_rust_module. PyO3 maps the ABI, Maturin builds the wheel. Python was designed to integrate with lower-level C libraries. Rust simply uses the same door. To Python, your compiled Rust is just another module.](https://blobs.talkpython.fm/guides/563/12.webp?cache_id=90ec0042)

How does Rust code end up as something you can `import`? It starts with Python itself. Python was designed from the get-go to integrate with lower-level languages, Christopher Trudeau explained. A good chunk of the standard library is a thin wrapper over the C standard library. The older parts are a bit creaky in their naming, and if you're a C programmer you recognize them on sight. (At least, as Michael Kennedy noted, they don't use Hungarian notation.)

The interface C extensions use works just as well for Rust. That's how Pydantic and Polars are built: the library is written in Rust and exposed through a very thin veneer, and Python treats it like any other module. Christopher's sense is that a lot of people find Rust easier to write than C for this job. There's a learning curve, but there are plenty of things it won't let you do, so "there's fewer foot guns."

The most common way to build that bridge is a crate called PyO3. It defines the interface between your compiled dynamic library (a `.so` or `.dll`) and Python's ABI, and it works in two directions:

- **Python types in Rust.** PyO3 has Rust objects that stand in for Python objects. A `PyInt`, for example, gives your Rust code access to a Python integer.
- **Rust types for Python.** It can also map ordinary Rust types for you, so a function that takes a 32-bit integer looks like it takes a normal integer from the Python side.

To choose what Python sees, you use macros, Rust's mechanism for changing how code gets compiled. You attach one of PyO3's macros to a regular Rust function or class, and it generates the mapping, including how the arguments line up. Compile it and everything lands in a `.so`. Then a tool like maturin handles the packaging, installing the result into a virtual environment or building it into a wheel so Python can find it. After that, it's just an import.

It doesn't have to be all or nothing. Can you write most of a package in Python and only the hot part in Rust? Yes, and Christopher pointed to Polars as an example. Watch closely when you install it and you'll see two wheels: a universal one that holds only Python code, and a platform-specific one with the compiled Rust library. He was clear that he's never looked at how Polars splits its code. And it doesn't have to be two wheels. Maturin will bundle a mixed Rust and Python project for you. Christopher sees it as no different from any other plugin mechanism; these are just the tools Rust gives you to build them.

> "And from Python's perspective, once that's installed in the right place, you just import foo from bar as you would with any other module and you've got access to it all."
>
> Christopher Trudeau

**Resources**

- [PyO3 user guide](https://pyo3.rs)
- [PyO3 on GitHub](https://github.com/PyO3/pyo3)
- [maturin](https://www.maturin.rs)
- [maturin on GitHub](https://github.com/PyO3/maturin)
- [Extending Python with C or C++](https://docs.python.org/3/extending/extending.html)
- [Polars](https://pola.rs)

## When to rewrite Python in Rust: don't, then measure

![The Golden Rule: Don't, then measure. A timeline chart where a tall Heroic Rust Rewrite spike saves only a tiny sliver of total time. Every call across the Python and Rust boundary has a cost, so chatty calls slow you down. Hand Rust big chunks of work, like Polars or Pydantic do, and take the answer back.](https://blobs.talkpython.fm/guides/563/13.webp?cache_id=07cbd31f)

Once you know you can call Rust from Python, it's tempting to start rewriting the slow parts. Michael Kennedy thinks there's plenty of low-hanging fruit: run a profiler and you may find that only about 20 lines of your code really matter.

Christopher Trudeau agreed, but with a catch. Every call from Python into Rust crosses a boundary, and crossing it has a cost. If your code bounces back and forth constantly, or your objects keep crossing over, the overhead adds up. Writing it in Rust doesn't guarantee it'll be faster. It could make it slower.

What works is the shape Polars and Pydantic use. Hand Rust a big chunk of work, let it crunch, and take the answer back. With Polars that's heavy number crunching. With Pydantic, Michael pointed out, the data arrives as JSON anyway and has to be transformed, so let that whole job happen in Rust.

But how do you know which part to hand over? Christopher's general advice on optimization is "don't and then measure." No matter how good you are at guessing up front, you'll eventually make the wrong assumptions. Build it, profile it, then find out where the time actually goes. Michael has a story or two to back that up. Way back in the day, he was so wrong about what was slow and what was fast that following his intuition would have meant months of bad choices.

Christopher brought up Amdahl's Law, and Michael pushed it to the extreme. Say 20% of your program's time is spent in one spot. Make that spot infinitely fast and the best you'll get is a program that's 20% faster. Now picture the heroic version: you move that code off a single CPU thread onto a GPU and make it a thousand times faster. If the whole thing takes five milliseconds, you've saved one millisecond. Hooray. And look how much harder your life is, and how much more expensive your deployment just got.

Measuring is getting easier, too. Michael is especially interested in the new sampling profiler in Python 3.15, which he was lining up an episode about with its authors, Pablo Galindo Salgado and László Kiss Kollár. What he likes about it is that you can attach it to a process already running in production, collect data for something like 30 seconds, and step away. Then you can see whether making one part faster would really be a big deal.

> "As programmers, I think we often get tied up in the, oh, wouldn't it be cool if I made this code three times harder to read and it'll be, you know, half a millisecond faster. And how often do you call that? Once a year?"
>
> Christopher Trudeau

**Resources**

- [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law)
- [PEP 799: a dedicated profiling package](https://peps.python.org/pep-0799/)
- [Python 3.15 sampling profiler docs](https://docs.python.org/3.15/library/profiling.sampling.html)
- [What's new in Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html)

## awesome-python-rs: find a Rust-backed library before you write one

![Don't Rewrite. Replace. A search bar for github.com/ritwiktiwari/awesome-python-rs links to orjson, cryptography and polars. Before you spend a weekend rewriting your slow JSON parser in Rust, check the awesome-list: someone has likely built a fast, drop-in, Rust-backed replacement already.](https://blobs.talkpython.fm/guides/563/14.webp?cache_id=9b7125ab)

The Rust book teaches you the language. But where do you go to see Rust actually used in the Python world, rather than in textbook examples? Christopher Trudeau's pick is awesome-python-rs, a massive list of tools and libraries in the Python space that use Rust under the hood. If you want to read real code, it's a great place to start digging. Michael Kennedy admitted he's a sucker for awesome lists. Want to watch a movie? Or he could poke around a new awesome list for the next half hour.

The list also puts optimizing in a new light. It includes some classic drop-in replacements built for speed, Christopher noted, among them a cryptography library and a JSON library, each standing in for functionality from the standard library. Because they're written in Rust, he said, they're "screamingly faster."

Michael had floated the idea of rewriting a slow bit of your code in Rust. But someone may already have done it as a library. So before you rewrite anything, check whether you can just swap your JSON parsing library and move on.

There's a trust angle here too, and it ties back to the worries about crates. Many of these Python libraries are thin wrappers, Christopher explained, sometimes just a few lines of API glue, around existing Rust libraries. Someone found a Rust library that does something they liked, wished it were in Python and exposed it as a module. That means you're not trusting somebody who spent a weekend rewriting JSON parsing, as Michael put it. You're leaning on a library the Rust community itself depends on (see the quote below).

Polars is the model case. It's a Rust library for Rust programmers, with a Python layer on top, so Rust people use it as well as Python people. In caramel apple terms, as Christopher said, there's caramel everywhere by the time you're done.

> "These are things with thousands of stars and many contributors because the Rust community themselves are using them, right?"
>
> Christopher Trudeau

**Resources**

- [awesome-python-rs](https://github.com/ritwiktiwari/awesome-python-rs)
- [orjson](https://github.com/ijl/orjson)
- [cryptography](https://cryptography.io/en/latest/)
- [Polars](https://pola.rs)

## Is Rust hard to learn, and will it make you a better Python developer?

![The Dual-Stack Mindset. Over a circuit board threaded with blue and orange light: fighting the borrow checker fundamentally changes how you think about software architecture. Learning Rust's strict memory rules makes you a highly deliberate, optimized Python developer, even if you never ship a compiled binary.](https://blobs.talkpython.fm/guides/563/15.webp?cache_id=9c31d01e)

Rust has a reputation as a hard language. Christopher Trudeau hears that all the time, and he didn't find it that way. He's careful how he says it, though. It isn't because he's so much smarter, he says. It's because of where he started. He was writing C at 13 or 14, back when there weren't many other choices, and went on to write device drivers and embedded systems. That's the closer-to-the-machine grounding Rust more or less expects you to have.

Michael Kennedy recognized the same thing in himself. He and Christopher are about the same age, technologically speaking, and Michael never came to Python or JavaScript without already knowing what to do with a `void**`. The memory model was never a surprise.

What about someone without that background? Christopher had been talking with Bob Belderbos of Pybites, who got into Rust recently after coming from PHP and Python. For Bob it was a whole other kind of experience. And yet he thinks it's made him a better Python developer, because now he thinks about memory in ways a pure Python background never asked of him (his words, as Christopher relayed them, are in the quote below).

So yes, the friction is real, especially for newer developers, and the [ownership](#rust-ownership-explained-for-python-developers) section covers where most of it comes from. But Bob's experience points at the payoff. The stack and the heap, who owns a value, how big a number is: Python handles all of it for you, and learning Rust is one way to find out what that handling involves, even if you never ship a compiled binary.

Ready to start? Christopher's first stop is rust-lang.org. He calls it remarkably well documented, and the Rust book is right there on the site, easy to follow and full of really good examples. If you'd rather learn Rust by comparison to the Python you already know, Christopher's own course, [Up and Running with Rust](https://training.talkpython.fm/courses/up-and-running-with-rust?utm_source=talkpythondeepdive), goes from zero through the stack, the heap and ownership to a compiled extension you import from Python with PyO3 and maturin. Michael called it both very educational and funny. For the Python side of the memory story, [Python Memory Management and Tips](https://training.talkpython.fm/courses/python-memory-management-and-performance-optimization?utm_source=talkpythondeepdive) covers how CPython actually allocates and frees memory. And since Polars kept coming up as the model Rust-backed library, [Polars for Power Users](https://training.talkpython.fm/courses/polars-for-power-users?utm_source=talkpythondeepdive) shows what that Rust core buys you in practice.

> "He actually thinks it's made him a better Python developer, because now he's thinking about memory and stuff like that a little more than you necessarily would if you've just come from Python."
>
> Christopher Trudeau

**Resources**

- [The Rust Programming Language](https://doc.rust-lang.org/book/)
- [rust-lang.org](https://www.rust-lang.org)
- [Pybites](https://pybit.es)

---

Listen to the full episode: https://talkpython.fm/episodes/show/563/getting-started-with-rust-as-python-devs
Episode show notes as Markdown: https://talkpython.fm/episodes/show/563/getting-started-with-rust-as-python-devs.md
