Python to Rust - Some early thoughts
I have been following Rust for very long time, I tried to learn Rust at least 3 times in the past 5 years. Last time I started learning rust, v1.36 was just released. I always started to learn from the online rust book (which is very nice), In last 3 attempts I could only reach till chapter 9. This time i.e. about 8 months back, I decided to learn Rust completely and slowly. So I got myself a Udemy course (pre-recorded) and just followed it. I can say, I know basic and can do non-vibe coding. Now to the topic of the post, but, before starting the post, a couple of quick disclaimers I used Python for 10+ years and Rust for very short time, so this is based on very limited Rust knowledge. Python is 30+ years old vs. Rust is ~10 year old, So new things often seem shiny! 1. Which packaging tool do you use? In Python, there are multiple packaging tools. You have, basic pip to latest uv, and in between you have poetry, hatch etc. I am personally comfortable with poetry but it's personal preference. As a team, It's not easy to agree on one tool when you have many options. On top of this, you need formatting, linting, test runners. Agreeing on tools sometimes can be daunting. It's hard to justify, in a 40K LOC project, which formatter to use ruff or black, unless you care about sub-second delay with black. In Rust, there is only one, Cargo! It does everything. (For good or bad) 2. Libraries and separation of concern In Python, I love pathlib library. It does everything, you can create path and then manipulate it. import pathlib as pl tmp_file = pl.Path("/tmp/this/is/new/dir/a.txt") tmp_file.is_file() # False tmp_file.parents.mkdir(parents=True) tmp_file.open("w").write("this is test file") # How do you delete it? # maybe, tmp_file.parents.rmdir() # nope, no option to delete dir-tree With one module, I created directory structure I created file and wrote text in it. It made it so simple. But, then in order to delete tree you need another module. also inverse of mkdir, i.e, rmdir they don't work in similar manner. import shutil shutil.rmtree(tmp_file.parent) In Rust, I did the same exercise and it required 2 different modules. use std::fs; // file-system use std::path::Path; fn main() { let path = Path::new("/tmp/this/is/new/dir/b.txt"); println!("{:?}", path.is_file()); // same as python /* Creating file and dir is handled by fs module (it can create and delete entire dir tree */ fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, "this is a test file").unwrap(); // fs::remove_dir_all(path.parent().unwrap()).unwrap() } Oh boy, but if you want to append to a file in Rust, it is not intuitive at all. also, I am not using fs::write to append to a file. let mut append = fs::OpenOptions::new().append(true).open(path).unwrap(); append.write_all(b"\n and this is another line").unwrap(); 3. Another example of library Python, In order put a thread to sleep/pause it, you would do import time time.sleep(3) I never questioned "why time lib that deals with time, is putting a thread to sleep?", until I did it in Rust Rust, In order put a thread to sleep, you would do use std::thread::sleep; use std::time::Duration; fn main() { sleep(Duration::from_sec(3)); // you have from_millis/micro/nano API to sleep for even shorter duration. } As thread will be put to sleep, sleep is in thread module. and you have to use Duration object from time to specify how long to sleep? 4. import this In Python, you have Zen of python, Its a philosophy that you can follow to write code. It was written in 1999! You look at the current python ecosystem and wonder if those are even followed during python development. Lets look at them, Note: I agree with those, just that they are sometimes hard to follow, also, since they are no concrete they are open to interpretation. Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those! I have some use cases where I wonder, Case 1 Lets look at, tmp_file = pathlib.Path("/tmp").joinpath("a.txt") # /tmp/a.txt # but same library also allows this, tmp_file = pathlib.Path("/tmp") / "a.txt"

I have been following Rust for very long time, I tried to learn Rust at least 3 times in the past 5 years. Last time I started learning rust, v1.36 was just released.
I always started to learn from the online rust book (which is very nice), In last 3 attempts I could only reach till chapter 9. This time i.e. about 8 months back, I decided to learn Rust completely and slowly. So I got myself a Udemy course (pre-recorded) and just followed it. I can say, I know basic and can do non-vibe coding.
Now to the topic of the post, but, before starting the post, a couple of quick disclaimers
- I used Python for 10+ years and Rust for very short time, so this is based on very limited Rust knowledge.
- Python is 30+ years old vs. Rust is ~10 year old, So new things often seem shiny!
1. Which packaging tool do you use?
In Python, there are multiple packaging tools. You have, basic pip
to latest uv
, and in between you have poetry
, hatch
etc.
I am personally comfortable with poetry
but it's personal preference. As a team, It's not easy to agree on one tool when you have many options.
On top of this, you need formatting, linting, test runners. Agreeing on tools sometimes can be daunting.
It's hard to justify, in a 40K LOC project, which formatter to use
ruff
orblack
, unless you care about sub-second delay with black.
In Rust, there is only one, Cargo! It does everything. (For good or bad)
2. Libraries and separation of concern
In Python, I love pathlib
library. It does everything, you can create path
and then manipulate it.
import pathlib as pl
tmp_file = pl.Path("/tmp/this/is/new/dir/a.txt")
tmp_file.is_file() # False
tmp_file.parents.mkdir(parents=True)
tmp_file.open("w").write("this is test file")
# How do you delete it?
# maybe, tmp_file.parents.rmdir() # nope, no option to delete dir-tree
With one module,
- I created directory structure
- I created file and wrote text in it.
- It made it so simple.
But, then in order to delete tree you need another module. also inverse of mkdir
, i.e, rmdir
they don't work in similar manner.
import shutil
shutil.rmtree(tmp_file.parent)
In Rust, I did the same exercise and it required 2 different modules.
use std::fs; // file-system
use std::path::Path;
fn main() {
let path = Path::new("/tmp/this/is/new/dir/b.txt");
println!("{:?}", path.is_file()); // same as python
/*
Creating file and dir is handled by fs module (it can create and delete entire dir tree
*/
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, "this is a test file").unwrap();
// fs::remove_dir_all(path.parent().unwrap()).unwrap()
}
Oh boy, but if you want to append to a file in Rust, it is not intuitive at all. also, I am not using fs::write
to append to a file.
let mut append = fs::OpenOptions::new().append(true).open(path).unwrap();
append.write_all(b"\n and this is another line").unwrap();
3. Another example of library
Python, In order put a thread to sleep/pause it, you would do
import time
time.sleep(3)
I never questioned "why time
lib that deals with time, is putting a thread to sleep?", until I did it in Rust
Rust, In order put a thread to sleep, you would do
use std::thread::sleep;
use std::time::Duration;
fn main() {
sleep(Duration::from_sec(3)); // you have from_millis/micro/nano API to sleep for even shorter duration.
}
As thread will be put to sleep, sleep is in thread
module. and you have to use Duration
object from time to specify how long to sleep?
4. import this
In Python, you have Zen of python, Its a philosophy that you can follow to write code. It was written in 1999!
You look at the current python ecosystem and wonder if those are even followed during python development. Lets look at them,
Note: I agree with those, just that they are sometimes hard to follow, also, since they are no concrete they are open to interpretation.
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
I have some use cases where I wonder,
Case 1
Lets look at,
tmp_file = pathlib.Path("/tmp").joinpath("a.txt") # /tmp/a.txt
# but same library also allows this,
tmp_file = pathlib.Path("/tmp") / "a.txt" # /tmp/a.txt
Its achieved by overridding __truediv__
, good example of polymorphism, but is it "explicit"?
Breaks:
- Explicit is better than implicit.
- Readability counts.
- Special cases aren't special enough to break the rules.
Case 2
set([1, 2, 3]) > set([3, 4, 5]) # is same as .is_subset()
Why is this allowed? Some AI tool generated this code with "greater than operator", which I could only understand by reading docs. If readability counts why have such options built in the language?
Breaks:
- Explicit is better than implicit.
- Readability counts.
Case 3
The following examples exist mostly for legacy reasons, but they are still part of the language.
1. os.path.joinpath or pl.Path?
2. glob.glob or pl.Path.glob?
3.
- "a %s", val
- "a {}".format(val)
- f"a {val}"?
Having multiple ways of doing same thing?
Breaks:
- There should be one-- and preferably only one --obvious way to do it.
- Although that way may not be obvious at first unless you're Dutch.
I don't have anything related to Rust like this, maybe due to my limited exp. or language is new compared to Python.
Now, coming to purpose of this post, I might never have questioned some of the implementation in my favorite language if I never tried same thing in another language.
It's good idea to learn a new language with a different philosophy every few years.