Back to writing
8 min read

Write Rust Like a Pythonista

5 Shortcuts to make writing Rust as a Python developer easier

Write Rust Like a Pythonista
Table of Contents

I’ve been writing Python for around 20 years, and have now done a few Rust projects. While Rust has been great for many reasons, I often miss how productive I felt in Python. Python feels fun, like building with Play-Doh. It’s squishy and I can just smoosh it into any shape I’d like. Rust, on the other hand, feels like I’m carving marble. Like I’m chiseling away at a solid block, revealing what the program wants to be.

While Rust does make faster, less error-prone programs, it can make programming feel like a slog. And it’s not just me. A lot of programming personalities are moving to Zig in 2025. Why? All they can really say is “Rust just isn’t fun”.

The problem, I think, is because Rust really, really wants replace C. The issue is that C is considered the performance gold standard, and a lot of talk around Rust is how close to C it can be for performance, while still wrapping the programmer in a soft blanket of safety.

But guess what? I’m a Python programmer. I don’t care if I can shave a couple nanoseconds off in Rust using some arcane feature. My Python program is several orders of magnitude slower. In fact, I’m happy doing the slowest possible thing in Rust if it means I can get the thing written faster. The Rust program will still be 100x faster and use 100x less memory than what I’m used to.

With that in mind, here’s a list of what I consider the most annoying parts of Rust, which can avoided if you’re already resigned to Python’s trade-offs. These are Rust tips for people who just want to get stuff done.

1. Don’t use async (or threads)

threads

If you think async in Python is a mess (I do), you might be surprised to know that Rust is even worse in some ways. Luckily, async Rust is largely a performance optimization. Since we’ve already established we aren’t worried about performance, we can sit this one out.

Unfortunately, every major Rust web framework is async. I wish there was a popular framework that just uses threads, but alas, that isn’t the world we live in. However, if you find async rust being forced onto you, there are escape hatches. For example, if the framework is using Tokio (think asyncio in Python) for its runtime, you can call spawn_blocking which prepares a nice thread for you to do your sync work in:

use tokio::task;
let res = task::spawn_blocking(move || {
    happy_sync_code();
}).await.unwrap();

That being said, also avoid threads when possible. Rust, unlike Python, has access to real threads, unbounded by a Global Interpreter Lock (GIL). Great for performance, bad for data races and mental overhead. You might be surprised by how fast a single-threaded Rust program can run. I was.

If you really need to run code in parallel, consider Rayon. If you structure your code right, it will take care of most of the gross thread issues you might have.

2. Just use String (and maybe &str)

Rust strings

Rust has so many damn string types. In Python there are 2, b"byte", and "string". These are roughly equivalent to Rust’s &[u8] and String types, respectively. The latter is a UTF-8 encoded string which is mostly what we want to be working with in Rust. When you need the others, it will be obvious, so just ignore them at first.

It’s OK to just call .to_string() whenever you see something besides String. It’s also OK to call .clone() whenever the borrow checker complains. Python copies data all over the place without telling anyone. You don’t need to feel bad doing it explicitly in Rust.

Alternatively, you can also experiment with &str. Using &str in a function definition is saying “take a reference to a String”. Although this is a small performance improvement (again, we don’t care), it also tells the compiler that you don’t intend to modify the string. If you do modify it by accident, the compiler will enforce this rule and refuse to compile. It’s like writing a little unit test inside your code. It also will reduce the amount of times you need to type .clone().

3. Let it fail

Unwrap

One of my favorite philosophies from Erlang is “Let it fail”. In Python I’ve applied this to web apps when there is a monitoring process with robust error reporting. Basically, if there’s a point in the code where an error could occur, but shouldn’t, don’t try to handle it. Just let it fail. The error reporting will trigger and the process will restart. The issue can then be investigated later when you’re awake and caffeinated.

Trying to handle every unexpected error case will cause an explosion of code and keep you from getting stuff done.

In Python, it’s easy to let code fail. Simply don’t add try/except blocks anywhere. In Rust, however, you typically have to explicitly tell the program it’s OK to implode into itself. This is because Rust doesn’t have exceptions, instead it returns errors as an Enum value; either an Option or a Result. In either case, you can slap an .unwrap() on that bad boy and move on with the value you wanted.

If an unexpected error gets returned, the program will crash and tell you exactly where the crash happened. If instead, you use the ? operator (as is normally suggested) to kick error handling down the road, you might end up with a stack trace far from where the error actually happened. Libraries like anyhow just make this error obfuscation easier, which is best to avoid at first.

4. Don’t be a fool, wrap your data

data wrap

Python’s dictionaries are so convenient, there’s even special syntax for them. It makes sense that Python programs use them all the time. Rust has HashMap. They are awful to work with in comparison. Same with any long-lived data structure.

This is because of Rust’s take on mutability. If you’d like to use a HashMap in your code, you’ll end up having to take a mutable reference (&mut) to it everywhere. Yes, this is much ‘safer’, but very annoying, especially when hacking things out.

Luckily, there’s a trick pattern called Interior Mutability which let’s you turn off the borrow checker. This trick involves encapsulating your HashMap (or whatever mutable data structure) in a RwLock. This allows you to pass around immutable references to the parent struct, and not have mut everywhere which leads to more borrow checking issues.

use std::sync::RwLock;
use std::collections::HashMap;

struct Dict {
    // Wrap in a RwLock
    map: RwLock<HashMap<String, i32>>,
}

impl Dict {
    fn new() -> Self {
        Dict {
            map: RwLock::new(HashMap::new())
        }
    }

    // Write operation, no &mut self reference!
    fn insert(&self, key: String, value: i32) {
        let mut map = self.map.write().unwrap();
        map.insert(key, value);
    }
    // Other methods
}

I’m sure there’s a crate somewhere that makes this easier, but nobody needs more dependencies. The major downside to this is you can only use one type for keys and values. However, you shouldn’t be mixing types in the same data structure anyway, you heathen.

5. Ignore the performance haters

mad

For better or for worse, Rust has a performance culture. This tip is about looking out for the subtle language in Rust documentation (especially The Book), which tries to shame you into using the most performant, but cumbersome or limited approach to a problem.

Performance shaming example from the Rust Book on Arc vs Mutex:

The reason is that thread safety comes with a performance penalty that you only want to pay when you really need to.

Just use Arc. It’s fine.

In general, if you just use whatever is the slowest way to do something, you’ll likely get more done with less compiler fighting. If you do somehow make your Rust code slower than Python, it’s almost certainly because of an architecture issue, and not which Mutex you picked. Or maybe programming just isn’t for you. And that’s OK.

Conclusion

As a Python developer, I took a lot of what it did for me for granted. I held off on learning Rust because it seemed too complicated. However, after figuring out what worked for me and ignoring what didn’t, I feel much more at home in this new ecosystem. This may not be The Right Way™️, but it’s still a good way.

If you follow these tips, (un tips?), and maybe clean up later, I think you’ll have a lot more fun too.

So, whenever you see someone write about Rust performance trade-offs, shaving nanoseconds off runtime, you can smile and think: “Shut up hippy. I’m a Python developer. You know nothing about performance trade-offs”.