What you'll learn: Top 10 habits to build, common pitfalls with fixes, a structured 3-month learning path, the complete PythonβRust "Rosetta Stone" reference table, and recommended learning resources.
Difficulty: π‘ Intermediate
flowchart LR
A["π’ Week 1-2\nFoundations\n'Why won't this compile?'"] --> B["π‘ Week 3-4\nCore Concepts\n'Oh, it's protecting me'"]
B --> C["π‘ Month 2\nIntermediate\n'I see why this matters'"]
C --> D["π΄ Month 3+\nAdvanced\n'Caught a bug at compile time!'"]
D --> E["π Month 6\nFluent\n'Better programmer everywhere'"]
style A fill:#d4edda
style B fill:#fff3cd
style C fill:#fff3cd
style D fill:#f8d7da
style E fill:#c3e6cb,stroke:#28a745
Use match on enums instead of if isinstance()
# Python # Rust
if isinstance(shape, Circle): ... match shape { Shape::Circle(r) => ... }
Let the compiler guide you β Read error messages carefully. Rust's compiler is the best in any language. It tells you what's wrong AND how to fix it.
Prefer &str over String in function parameters β Accept the most
general type. &str works with both String and string literals.
Use iterators instead of index loops β Iterator chains are more idiomatic
and often faster than for i in 0..vec.len().
Embrace Option and Result β Don't .unwrap() everything. Use ?,
map, and_then, unwrap_or_else.
Derive traits liberally β #[derive(Debug, Clone, PartialEq)] should be
on most structs. It's free and makes testing easier.
Use cargo clippy religiously β It catches hundreds of style and correctness
issues. Treat it like ruff for Rust.
Don't fight the borrow checker β If you're fighting it, you're probably structuring data wrong. Refactor to make ownership clear.
Use enums for state machines β Instead of string flags or booleans, use enums. The compiler ensures you handle every state.
Clone first, optimize later β When learning, use .clone() freely to
avoid ownership complexity. Optimize only when profiling shows a need.
| Mistake | Why | Fix |
|---|---|---|
.unwrap() everywhere | Panics at runtime | Use ? or match |
| String instead of &str | Unnecessary allocation | Use &str for params |
for i in 0..vec.len() | Not idiomatic | for item in &vec |
| Ignoring clippy warnings | Miss easy improvements | cargo clippy |
Too many .clone() calls | Performance overhead | Refactor ownership |
| Giant main() function | Hard to test | Extract into lib.rs |
Not using #[derive()] | Re-inventing the wheel | Derive common traits |
| Panicking on errors | Not recoverable | Return Result<T, E> |
Operation Python 3.12 Rust (release) Speedup
βββββββββββββββββββββ ββββββββββββ ββββββββββββββ βββββββββ
Fibonacci(40) ~25s ~0.3s ~80x
Sort 10M integers ~5.2s ~0.6s ~9x
JSON parse 100MB ~8.5s ~0.4s ~21x
Regex 1M matches ~3.1s ~0.3s ~10x
HTTP server (req/s) ~5,000 ~150,000 ~30x
SHA-256 1GB file ~12s ~1.2s ~10x
CSV parse 1M rows ~4.5s ~0.2s ~22x
String concatenation ~2.1s ~0.05s ~42x
Note: Python with C extensions (NumPy, etc.) dramatically narrows the gap for numerical work. These benchmarks compare pure Python vs pure Rust.
Python: Rust:
βββββββββ βββββ
- Object header: 28 bytes/object - No object header
- int: 28 bytes (even for 0) - i32: 4 bytes, i64: 8 bytes
- str "hello": 54 bytes - &str "hello": 16 bytes (ptr + len)
- list of 1000 ints: ~36 KB - Vec<i32>: ~4 KB
(8 KB pointers + 28 KB int objects)
- dict of 100 items: ~5.5 KB - HashMap of 100: ~2.4 KB
Total for typical application:
- Python: 50-200 MB baseline - Rust: 1-5 MB baseline
// Problem: trying to iterate and modify
let mut items = vec![1, 2, 3, 4, 5];
// for item in &items {
// if *item > 3 { items.push(*item * 2); } // β Can't borrow mut while borrowed
// }
// Solution 1: collect changes, apply after
let additions: Vec<i32> = items.iter()
.filter(|&&x| x > 3)
.map(|&x| x * 2)
.collect();
items.extend(additions);
// Solution 2: use retain/extend
items.retain(|&x| x <= 3);
// When in doubt:
// - &str for function parameters
// - String for struct fields and return values
// - &str literals ("hello") work everywhere &str is expected
fn process(input: &str) -> String { // Accept &str, return String
format!("Processed: {}", input)
}
// Python one-liner:
// result = [x**2 for x in data if x > 0]
// Rust equivalent:
let result: Vec<i32> = data.iter()
.filter(|&&x| x > 0)
.map(|&x| x * x)
.collect();
// It's more verbose, but:
// - Type-safe at compile time
// - 10-100x faster
// - No runtime type errors possible
// - Explicit about memory allocation (.collect())
// Rust has no REPL. Instead:
// 1. Use `cargo test` as your REPL β write small tests to try things
// 2. Use Rust Playground (play.rust-lang.org) for quick experiments
// 3. Use `dbg!()` macro for quick debug output
// 4. Use `cargo watch -x test` for auto-running tests on save
#[test]
fn playground() {
// Use this as your "REPL" β run with `cargo test playground`
let result = "hello world"
.split_whitespace()
.map(|w| w.to_uppercase())
.collect::<Vec<_>>();
dbg!(&result); // Prints: [src/main.rs:5] &result = ["HELLO", "WORLD"]
}
cargo build, cargo test, cargo clippyOption<T> and Result<T, E> until naturalclap and serdeaxum and tokio| Python | Rust | Chapter |
|---|---|---|
list | Vec<T> | 5 |
dict | HashMap<K,V> | 5 |
set | HashSet<T> | 5 |
tuple | (T1, T2, ...) | 5 |
class | struct + impl | 5 |
@dataclass | #[derive(...)] | 5, 12a |
Enum | enum | 6 |
None | Option<T> | 6 |
raise/try/except | Result<T,E> + ? | 9 |
Protocol (PEP 544) | trait | 10 |
TypeVar | Generics <T> | 10 |
__dunder__ methods | Traits (Display, Add, etc.) | 10 |
lambda | |args| body | 12 |
generator yield | impl Iterator | 12 |
| list comprehension | .map().filter().collect() | 12 |
@decorator | Higher-order fn or macro | 12a, 15 |
asyncio | tokio | 13 |
threading | std::thread | 13 |
multiprocessing | rayon | 13 |
unittest.mock | mockall | 14a |
pytest | cargo test + rstest | 14a |
pip install | cargo add | 8 |
requirements.txt | Cargo.lock | 8 |
pyproject.toml | Cargo.toml | 8 |
with (context mgr) | Scope-based Drop | 15 |
json.dumps/loads | serde_json | 15 |
What you'll miss from Python:
- REPL and interactive exploration
- Rapid prototyping speed
- Rich ML/AI ecosystem (PyTorch, etc.)
- "Just works" dynamic typing
- pip install and immediate use
What you'll gain from Rust:
- "If it compiles, it works" confidence
- 10-100x performance improvement
- No more runtime type errors
- No more None/null crashes
- True parallelism (no GIL!)
- Single binary deployment
- Predictable memory usage
- The best compiler error messages in any language
The journey:
Week 1: "Why does the compiler hate me?"
Week 2: "Oh, it's actually protecting me from bugs"
Month 1: "I see why this matters"
Month 2: "I caught a bug at compile time that would've been a production incident"
Month 3: "I don't want to go back to untyped code"
Month 6: "Rust has made me a better programmer in every language"
Challenge: Review this Rust code (written by a Python developer) and identify 5 idiomatic improvements:
fn get_name(names: Vec<String>, index: i32) -> String {
if index >= 0 && (index as usize) < names.len() {
return names[index as usize].clone();
} else {
return String::from("");
}
}
fn main() {
let mut result = String::from("");
let names = vec!["Alice".to_string(), "Bob".to_string()];
result = get_name(names.clone(), 0);
println!("{}", result);
}
Five improvements:
// 1. Take &[String] not Vec<String> (don't take ownership of the whole vec)
// 2. Use usize for index (not i32 β indices are always non-negative)
// 3. Return Option<&str> instead of empty string (use the type system!)
// 4. Use .get() instead of bounds-checking manually
// 5. Don't clone() in main β pass a reference
fn get_name(names: &[String], index: usize) -> Option<&str> {
names.get(index).map(|s| s.as_str())
}
fn main() {
let names = vec!["Alice".to_string(), "Bob".to_string()];
match get_name(&names, 0) {
Some(name) => println!("{name}"),
None => println!("Not found"),
}
}
Key takeaway: Python habits that hurt in Rust: cloning everything (use borrows), using sentinel values like "" (use Option), taking ownership when borrowing suffices, and using signed integers for indices.
End of Rust for Python Programmers Training Guide