Rust and Python
Should you use Rust to speed up your Python code? Well, it depends on what sort of work you’re doing.
I’m doing CPU work I know I need Rust
Are you doing CPU work in Python and it’s just to slow? Rust with
PyO3 might be a decent option, but let’s not be too hasty.
Before you jump to that, you might want to do some profiling with something
like austin to see where you’re keeping
the CPU busy. You might be surprised to find that a lot of the work is in a
copy or deepcopy that you can easily refactor away with a little thinking. You
might have one little container object and you’re spending a lot of time in
attribute lookup on that container object that you can optimize by fiddling
around with __slots__
. Maybe you
wrote some O^2 code that could be refactored into O(log N) with a little
caching, maybe toss a set in there or mess around with
functools.cache
.
It’s also entirely possible that you can just stand up another Python process
and communicate to it. Could you put that CPU work that you’ve already written
into another Python process and call it over the network? What if that other
process was written in a faster language? What if that other process was on the
same machine and you talked to it through a Unix socket? Would the
serialization and deserialization tax negate any sort of wins? Did you already
replace the stdlib json
module with orjson
in your project?
Is your CPU work mostly numerical, scientific, or data work that could be done with an existing specialized performance library like SciPy, NumPy, Pandas, or Polars?
None of that helps me I really need a compiled Python module
Why? Why? Why?
If you’ve gotten here you’ve convinced yourself the other options are all bad, but the ability to name a bad thing does not negate the possible existence of a good thing. There might be something out there that already exists that you can use to avoid getting into the territory of writing a compiled Python module. So what even IS it about writing a compiled Python module that helps you?
If you’re in this territory, a compiled Python module has these added benefits:
- It’s all the same process, so you don’t need to stand up another server/node/pod/process/service or whatever you call the fundamental unit of isolation in your infrastructure, this means less up-front work overall, less capacity planning, fewer moving parts to operate and have to worry about during an incident, etc
- Since it’s a shared-memory model, you don’t pay a serialization tax when going from Python to the other environment. This is a huge differentiator! Does it matter to you?
These are the classic wins of monoliths in the monolith versus microservice religious wars. If you’re already operating under a largely monolithic architecture, these benefits might slot in nicely with your existing project. If you’re already doing microservices, think long and hard about why a compiled Python module is better than just writing a Rust microservice instead.
Of course, these benefits are also true of other compiled-Python environments. Have you ruled out writing your Python module in C or C++? How about compiling Python code into machine code with Cython or Numba?
Here are some benefits of using Rust instead of those other things:
- Now that you’re binding Rust to Python on your own, you have the entire ecosystem of Rust crates at your disposal.
- Developers who use Rust tend to really like it
- Something something type safety, memory safety, superior latency characteristics, memory utilization, you won’t get hacked, etc. I’m hand-waving a bit here because this isn’t really meant to be a Rust-evangelism post so much as it’s meant to be a checklist of questions to ask yourself before jumping into PyO3.
- There’s a good chance you already depend on the PyO3 ecosystem; it’s used under the hood to implement Pydantic
(specifically the Rust code is in
pydantic-core). If you’ve used
orjson
to speed up your json serialization, that’s Rust internally too. - The Astral team writes their Python tooling in Rust; if
you use
ruff
for linting (you should, it’s great) oruv
as a pip replacement then you’re already using Rust tools for Python. (as an aside, why are so many new high-performance Python tools and libraries like Pydantic and Polars and orjson written in Rust, and why didn’t that happen with Cython or Numba?)
Neat, ok, fine, what’s the catch:
- Making changes requires a build process that may not fit how you’re using Python today
- Once you’ve compiled your Rust code into a Python module, how do you distribute it? Distributing the wheel privately would require a pypy server, maybe there’s an Artifactory server somewhere you can comandeer. If your project is all local or all open source then never mind here, you’re doing great.
- You now have a version-matching problem: how do you version your compiled Python module and ensure your Python project is always up to date with the module?
- Well now you’re writing Rust, do you know how to write Rust? How about the other people you’re working with? The learning curve can be pretty steep, you know.
- Python values are all shared; a non-shared (owned) Rust value cannot easily cross the Rust-Python bridge.
- The async situation can be a bit taxing. Rust is hard, async Rust is harder,
and binding
tokio
andasyncio
together is harder still, because now you have two async runtimes to worry about and manage. - Whatever work you’ve done to operationalize your Python logic you might have to replicate in Rust and learning how to operationalize and debug Rust is a whole can of worms
Great, Here I go, gonna rewrite all my business logic in Rust!!!
Wait wait wait, stop, stop, stop. Here’s where discussions can often get off
the rails. What is actually your problem with Python? Is it the way the type
system works? Do you hate the existence of
None
,
exceptions, and
inheritance? Do you not
like significant whitespace? Are you made about function
coloring?
These are, frankly, not great reasons to adopt Rust and PyO3 into an existing
codebases. Sure, those things can be legitimate concerns (well, except for the
significant whitespace thing, get over it), but those aren’t on their own good
justification for rewriting existing business logic.
Can a highly-experienced Rust engineer be productive writing business logic in Rust? Absolutely. The argument that you can’t write business logic in Rust doesn’t really convince me of much; people making this argument generally don’t know how to write decent Rust code. It is certainly possible to write business logic in Rust, and experiencd Rust engineers can be extremely productive doing exactly that. That is not itself justification for writing your business logic in Rust, though, especially when stacked against Python, which is arguably the single best programming language for expressing business logic of all time. Why else would it be the world’s most widely know programming language? It’s not easy to operationalize, it’s not particularly performant, and it’s got terrible safety parameters. Python is great because it’s comparatively easy to learn and can succinctly express business logic. Why write your business logic in something else?
Generally speaking, Rust is a relatively poor choice for expressing business logic compared to other programming languages, because of the realities of the software industry and how hiring and engineering career paths work. Outside of specialized domains, engineers tend to move farther away from business logic as they get more senior. Think about it this way: which of these two stories seems more likely?
- a database engineer becomes more senior and eventually learns how to make a web app
- a web app engineer becomes more senior and eventually learns how to make a database
I started my career exclusively writing business logic in web apps, and as I’ve gotten more senior, I have written less and less business logic as my career has grown. My guess is this is probably a common story! I imagine a lot of engineers’ careers look like this. That’s not a value judgement about any engineers’ level of inherent skill or intelligence in any way: it is how the software industry as a whole tends to be organized.
Rust has a known steep learning curve and anyone who tells you that Rust is easy to learn is either fooling you, fooling themselves, has never written Rust in production, or is a statistical outlier. In general, learning Rust takes a decent amount of time and effort. Are you going to have all of the people on your Python project learn Rust? Probably not. You’ll probably have a portion doing that work. That portion should be the people on the teams that are charged with building capabilities, not the people on the teams that are charged with building end-user product features. As it turns out, the problem is not that Rust cannot express business logic, but that the population of people whose primary job function is to write business logic are not the population of people for whom the steep learning curve will yield a positive return on invesment.
So are you for this or against this?
It’s got some cool properties and there’s definitely a real reason why the Rust-Python mix is gaining popularity. It can be an extremely powerful combination. At the end of the day, it’s just one technique among many, but not to waffle on this too much: yeah I’m for it, I think it’s rad as hell, I wouldn’t have written this post if I didn’t think that, but you have to judge on a project-by-project and team-by-team basis whether it’s a good fit for what you’re working on. Hopefully this post has given you some clarity on what sort of questions you can ask yourself to help weigh whether or not your project is a good candidate for mixing Python and Rust.