Rust has become the secret recipe leveling up the performance of many tools in our Python ecosystem.
You really can feel the boost like a goosebump ✨ when you start using these Rust-powered tools.
Among them are: ruff
🐺 , uv
⚡, and polars
❄️ which are fired up by Rust and have been significantly 🚀 speed up our python workflows.
ruff
is used to format and lint your python code, and uv
and polars
can be thought as a faster, more ergonomic version of pip
and pandas
, respectively.
The performance boost I’ve experienced has made me genuinely excited to learn Rust 🦀. Coming from a python 🐍 background, I want to share my journey of getting started with Rust. So let's jump right in!
The official documentation for Rust is rust-lang.org. It's like python.org for python.
First, we need to install rustup
, which is a Rust installer
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Once installed, check for stable release with rustup update stable
.
Check the version with rustc --version
.
With rustup
, you can manage multiple Rust versions like with uv
for python.
For example, you can install a specific rust version for a project with:
rustup install 1.70.0
rustup override set 1.70.0
Once Rust is installed, it comes also with cargo
, it's like pip
for python but with a few extra features.
ruff
equivalent in Rust is cargo fmt
to format the code, and cargo clippy
is like ruff
to lint the code.
fmt
is to ensure that our code follows a consistent style or for me It's a lazy shortcut: I set my editor to automatically format on save and let fmt
do the right thing.
lint is always a confusing word for me, but in short it's a way to check for coding best practices.
As a simple example if we have unused variables in our code, clippy
will warn us.
The nice thing, they also provide suggestions to fix the issue, which is to prefix the variable with an underscore _
.
--> src/main.rs:11:9
|
11 | let hello = "Hello";
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_hello`
|
= note: `#[warn(unused_variables)]` on by default
Let's create a simple hello world program, hello-rust
.
Our first step is to run cargo
, which we'll use often with Rust.
# Start a new Rust project
cargo new hello-rust
cd hello-rust
The project is now created and we can directly go to src/main.rs
.
This is where our application code starts.
# Change to hello-rust
fn main() {
println!("Hello, Rust!");
}
To run the program, cargo run
.
This is kind of similar to python main.py
in python.
The difference between Rust and python is that Rust is a compiled language, whereas python is an interpreted language.
In the interpreted language, the code is executed by Python interpreter so it can be run directly.
With Rust, the code is first compiled into machine code by rustc
(Rust compiler) in a build process.
The advantage of compiled Rust is that the resulting program runs independently, without needing the Rust compiler installed, making distribution easier. On the other hand, running Python code requires the Python interpreter to be installed on the machine or server. Rust compiler also helps catch many errors at compile time, making our code safe to run. With safety guarantee, the compiled code is run with lesser checks, thus boosting the performance of our code.
When we run cargo run
, Rust first compiles the code into machine code, which you can find at target/debug/hello-rust
, and then executes it.
You should see Hello, Rust! printed in the terminal.
Let's create a small CLI application that count the lines of a file.
First, we will add a dependency to our application.
This is to help parsing the command line arguments, we will use clap
crate.
We can find the dependency in crates.io, which is the package registry for Rust, like pypi.org
for python.
In our Cargo.toml
similar to pyproject.toml
in python, we will add the dependency.
# Add this to your Cargo.toml
[dependencies]
clap = { version = "4.0", features = ["derive"] }
Let's update our src/main.rs
to use clap
to parse the command line arguments.
use clap::Parser;
use std::fs::read_to_string;
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, about = "Counting lines")]
struct Cli {
path: PathBuf,
}
fn main() {
let args = Cli::parse();
let content = read_to_string(&args.path).expect("File cannot be read");
let result = content.lines().count();
println!("{}", result);
}
At the top of the file, we have a list of use
statements like Python’s import. It brings external functionality into your code.
The #[…]
syntax is called an attribute in Rust.
For now, you can think of it like a decorator (sort of) in Python.
In this example, #[derive(Parser)]
automatically generates code so that our Cli struct can parse command-line arguments.
The #[command(version, about = "Counting lines")]
attribute adds metadata for a nice help message, including version info and also description.
A struct
in Rust is similar to a Python dataclass; it's a way to create a custom data type.
Our struct Cli
defines a custom data type similar to a Python dataclass, with PathBuf
representing a filesystem path.
Rust introduces ownership rules for memory safety while Python uses a garbage collector.
Here, we use a reference &args.path
so that our function borrows the path for temporary use.
The expect
method is used to handle errors gracefully.
If read_to_string
fails, the program will print "File cannot be read"
instead of panicking with a generic message.
Now we can run our application with cargo run -- <file-path>
.
The --
is used to separate the arguments for cargo
and the arguments for our application.
For example, let's first check the version
and help
from our application.
$ cargo run -- --version
hello-rust 0.1.0
$ cargo run -- --help
Counting lines
Usage: hello-rust <PATH>
Arguments:
<PATH>
Options:
-h, --help Print help
-V, --version Print version
Let's now give it a spin,
cargo run -- src/main.rs
It should print the number of lines in src/main.rs
.
Huhu 🥳, so now we have written a CLI application in Rust to count lines with proper --help
and --version
flags.
As you also may notice when run cargo run
, we see that it is unoptimized
.
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/hello-rust --help`
This is because we are running in debug
mode and it is intended only for development.
When we are ready to ship our application, we can run cargo build --release
to build the optimized version of our application.
Now our application is shipped in target/release/hello-rust
and we can run it directly with ./target/release/hello-rust <file-path>
.
cargo
is your friend and it is like pip
that helps you manage your Rust project.rustup
got you covered with rustup doc --book
to read the official Rust book free and offline.