I’ve been dabbling with Rust for about two years now, and it’s rapidly become my favourite language. I even got a chance to use it at work recently: a piece of our optimization algorithm was just too slow for our needs as it needs to run on years of sales data and for eight different countries so it has to be pretty fast. So I tried rewriting it from Python to Rust which brought a runtime reduction by a factor 10-100x.
Of course, this didn’t happen just because Rust is fast compared to Python (I mean, it is!) but also because Rust forced me to handle things differently, made me question some assumptions, some operations which were not efficient, and even bugs which didn’t pass the type system.
This was such a success I even got to host an internal workshop on how to use Rust, aimed at data scientists. In a large organization like IKEA, knowledge sharing is crucial so we can all benefit from others’ experiments. Most of our data scientists are not familiar with Rust, and rightfully so. In data science, Python is king! Its ecosystem is hard to match, and a scripting language is ideal for the kind of prototyping we have to deal with. But eventually, when things need to be deployed to production, resource and runtime requirements can catch up with you: that’s where Rust comes in and provides a great alternative for performance-critical code.
Why not use Go?
This is an actual, valid question I got a few days ago. Few coworkers know Rust, but most of our engineers are fluent in Go, so why choose Rust? Well the first reason, and the motivation behind this article, is because Rust is a fantastic, reliable language. But while that’s a good reason for a personal project, it’s not always enough in a business environment! The main reason behind the choice was how easy it was to interoperate Rust code with Python using PyO3. As data scientists, our main applications are generally big Python monoliths, and being able to encapsulate hot paths in Rust, bind them inside a Python library and then just call them from your application is a real boon.
I was happy to see how much people resonated with the aspects of Rust which I love. Then, more recently, I saw this piece on the official Rust blog and thought I would also put my contribution out there.
I’ll be discussing a fair amount of the language in this post, so if you need to learn more about Rust or get a refresher on the language, here are some great references:
- The Rust book — The official Rust reference.
- Comprehensive Rust — A Rust course from the Google Android team.
- A half-hour to learn Rust — A fantastic (but old) reference to the language.
Rust is reliable
One of the crucial aspects which I love about Rust is how reliable it is. It’s quite a common thing that if your Rust code compiles, then it just works. Of course, getting it to compile can be harder, the type system can be unforgiving, the borrow checker ruthless, but all this is worth it. You remove so much ambiguity at compile-time that whole classes of bugs just disappear:
- wrong-type bugs: write some Python and you’ll see how much that happens,
- memory safety bugs: these are less of a problem if you come from a garbage-collected language, but you also gain more control on resource management without having to keep track of references everywhere,
- data race bugs: the borrow checker makes most of these impossible, and concurrency becomes almost trivial.
If you’ve worked with languages like Python, this can feel like a breath of fresh air. I love how versatile Python is, but its dynamism has a cost, and not just performance-wise. I never realized how much time I spent debugging Python code at runtime, navigating through breakpoints to track the part where my state becomes corrupted. This is quite an exhausting way of coding.
In Rust, I don’t have to worry about this as much, the language is built in such a way that most bugs are caught at compile-time. Of course you might still have some logic bugs (the operations are correct, just not what you really need to be doing), and there Rust does not always make your life easier (I’d even argue runtime debugging is harder so that’s a tradeoff), but in my experience, other aspects of the type system can truly help you there.
Rust is explicit
This is a more discreet aspect of Rust (everyone tends to focus on memory safety and performance), but one that neatly complements the strictness of the borrow checker. Whereas the latter removes common memory-related bugs, the former can also help you write better code, logic-wise.
By “explicit”, I mean that the type system often forces you to handle all possible cases and scenarios, and won’t compile otherwise. For example, enum are Rust’s union types. If you need to express that a variable can be one of many possibilities, that’s what you reach for. In Python, if you use type hints (which you should!), then you would use Union, but nothing would really force you to handle every case. Sure, mypy or pyright might complain, but that’s just a band-aid. And that’s because Python is only concerned with duck typing: as long as whatever object you plug in your function implements the right behaviours, its type does not matter. In Rust, this is not true. Not only is Rust’s typing strict, but if your variable is an enum, you must match all cases to use it.
This comes in handy with two special enums: Option<T> and Result<T, E>. These are Rust’s way to deal with missing values and fallible operations. An option-wrapped variable can either be Some(T) or None, and you must handle the option wrapper to access the value. This is unlike Python where if you had a x: int | None, you could just call x + 1, completely forgetting to handle the None branch. But in Rust, this isn’t optional, and if you’ve ever had this bug happen (haven’t we all), this is one more thing caught at compile-time.
let can_be_missing: Option<i32> = some_function();// I want to use the integer, but I need to remove the Option firstlet can_be_missing: i32 = can_be_missing.unwrap_or(0); // I'm fine with 0 if missing// Now I can use the value!The error handling case is even more powerful in my opinion. When you get a result, it can either be Ok(T) or Err(E). I won’t get into details of how error types are defined (it’s a bit messy in the standard library, and I usually resort to anyhow or thiserror crates depending on use cases), but here again if you want to access your data, you must deal with the error branch too! Now here is where Python really bungles it. With exceptions, you have absolutely no way in the “type system” (even with type checkers) to make sure you don’t forget one. Your only options are to trust the docs (and it’s rarely well documented there) or dig inside the code.
let can_fail: Result<i32, SomeError> = how_old_are_you();match can_fail { Ok(age) => do_something_with_age(age), Err(e) => { // Do something with the error... }}This might sound like a lot of work since you always need to use pattern matching or methods like .unwrap_or() on everything to get to your data. But the tradeoff here is predictability. I don’t want to be surprised by my code, and Rust helps greatly there.
Rust is functional
Note how all the above was made possible by the fact that Rust’s enums can hold data: they are an algebraic data type, which is a fancy way to say that not only do they represent unions, but each variant is its own type with its own data.
This is one of the aspects which shows the influence of functional programming languages like OCaml on Rust. It’s very nostalgic to me: when I was in university, the curriculum somehow covered OCaml (probably a French academia thing), and it was pretty cool. Later on, during my PhD, I had to deal with a lot of Wolfram Mathematica which also is very functional. Yet I don’t consider myself an expert in this topic at all, so don’t think you need to be to appreciate this part of Rust.
Practically speaking, it just means you don’t have to write Rust like you would write C. You can, but it might not be the most idiomatic: particularly when it comes to enums and iterators. Enums we already covered above, but iterators are new here. They’re a neat piece of functional programming.
What would you say is the most common marker of imperative programming?
To me, it’s for loops. In functional programming, you don’t really have for loops. Instead, you iterate over collections of elements: all you need is:
- a starting point
x, - a way to go to the next element
next().
That’s all iterators are! Then you can make them complex, compose operations like filter, map, etc., all of which just change the way you get the to the next element really. Of course once you have built up your iterator, you need to somehow convert it back to something usable. In Rust iterators are lazy: this means that all the filters, map, chain or whatever you did don’t actually do any work, they just change the recipe of the iterator. But when you call an accumulating method like collect or sum, you then go through the entire iterator and return a value.
And fundamentally, this is equivalent to a for loop, just written differently!
let sum_squares_even_numbers: i32 = (1..100) .filter(|x| x % 2 == 0) .map(|x| x * x) .sum() // Operation happens here
// is equivalent to
let mut total: i32 = 0;for x in 1..100 { if x % 2 == 0 { total += x * x; }}// Redefinition to remove mutabilitylet sum_squares_even_numbers = total;Rust has amazing tooling
Okay if you’re still with me, this is a simple one. Rust comes with a great developer experience. The dependency and package manager cargo does it all: building, dependency management, testing, formatting, etc. And not only does it do everything, but it does it well! After years of dealing with Python dependency nightmare (thank you uv for existing), it’s pleasant to have useful developer tools bundled with the language (though note that some of these features do require a separate install call).
What I don’t like about Rust
Okay I’ve been raving for a while on why Rust is amazing, so I would be remiss if I didn’t also present the other side. So, what are the downsides to Rust?
First of all, because it’s a compiled language, there is a lot more friction to getting code running (which is why Rust is not great for prototyping in general), and compile-times can be really slow, especially if you’re using heavy crates like Polars or PyO3. On that last point, note that there is some work being done on this front but I fear some of it is just inherent to how Rust is built (mostly polymorphism and crate features).
Another downside of Rust is debugging. I heard it’s going to get better in the next version, but currently Rust can be a little opaque for runtime debugging. It’s doable of course, but you won’t get the same developer experience you would in Python. Complex types like dataframes are hard to inspect at runtime, which can be a deal breaker for data products for example.
Conclusion
I’ve been programming for about fifteen years now, and most of that time I spent with interpreted languages. My first foray into coding was in C, but as a 13 year-old, I felt the language to be too terse and complex for a hobby, so I switched to web development for a few years. Most non-markup coding I was doing then was in javascript, before it took over the world, before react, when jQuery was king. I remember when node arrived, no documentation, playing with Express blind. Then I moved to Python when studying physics and kept going with it for a decade.
This is all to say that I’m not a systems programming guy, I don’t have a proper computer science background, the closer to the metal we get the less I know. Yet when I started with Rust, I immediately liked it. Even though I didn’t feel like the target audience, even though I only knew the world of garbage-collected interpreted languages, I quickly learnt to love coding with Rust. If you already love or hate Rust, this piece probably didn’t bring you anything new. But if you’re on the fence or just curious, I hope I managed to give you a better idea of what to expect and what this language can bring you, so you can make your own opinion too.