Python to Rust: A Transition Guide
This chapter is specifically for Python developers learning Rust. We'll compare concepts, syntax, and patterns to help you build on your Python knowledge while understanding Rust's unique approach.
Core Philosophy Differences
| Aspect | Python | Rust |
|---|---|---|
| Type System | Dynamic, duck typing | Static, explicit types |
| Memory Management | Garbage collection | Ownership system (no GC) |
| Mutability | Mutable by default | Immutable by default |
| Error Handling | Exceptions | Result/Option types |
| Compilation | Interpreted | Compiled to native code |
| Performance | Slower, optimized for dev speed | Fast, zero-cost abstractions |
| Concurrency | GIL limits true parallelism | Fearless concurrency |
Quick Syntax Comparison
Variables and Basic Types
Python:
# Variables (mutable by default)
name = "Alice"
age = 30
price = 19.99
active = True
# Type hints (optional)
name: str = "Alice"
age: int = 30
Rust:
// Variables (immutable by default)
let name = "Alice";
let age = 30;
let price = 19.99;
let active = true;
// Mutable variables (explicit)
let mut count = 0;
count += 1;
// Type annotations (usually inferred)
let name: &str = "Alice";
let age: i32 = 30;
Functions
Python:
def greet(name: str) -> str:
return f"Hello, {name}!"
result = greet("Alice")
Rust:
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
let result = greet("Alice");
Control Flow
Python:
# If statement
if age >= 18:
print("Adult")
elif age >= 13:
print("Teen")
else:
print("Child")
# For loop
for i in range(5):
print(i)
# While loop
while count < 10:
count += 1
Rust:
// If expression (returns a value)
let category = if age >= 18 {
"Adult"
} else if age >= 13 {
"Teen"
} else {
"Child"
};
// For loop
for i in 0..5 {
println!("{}", i);
}
// While loop
while count < 10 {
count += 1;
}
Collections
Python:
# List (dynamic array)
numbers = [1, 2, 3, 4, 5]
numbers.append(6)
# Dictionary
user = {"name": "Alice", "age": 30}
# Set
unique = {1, 2, 3}
# Tuple
point = (10, 20)
Rust:
// Vec (dynamic array)
let mut numbers = vec![1, 2, 3, 4, 5];
numbers.push(6);
// HashMap
use std::collections::HashMap;
let mut user = HashMap::new();
user.insert("name", "Alice");
user.insert("age", "30");
// HashSet
use std::collections::HashSet;
let mut unique = HashSet::new();
unique.insert(1);
unique.insert(2);
// Tuple
let point = (10, 20);
Error Handling
Python:
def divide(a: int, b: int) -> float:
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")
Rust:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}
match divide(10, 0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
// Or with ? operator
fn main() -> Result<(), String> {
let result = divide(10, 2)?;
println!("Result: {}", result);
Ok(())
}
Key Concepts Python Developers Need to Learn
1. Ownership and Borrowing
The Challenge: Python uses reference counting and garbage collection. Rust uses ownership.
Python (references are implicit):
def modify_list(lst):
lst.append(4) # Modifies original
numbers = [1, 2, 3]
modify_list(numbers)
print(numbers) # [1, 2, 3, 4]
Rust (ownership must be explicit):
fn modify_vec(vec: &mut Vec<i32>) {
vec.push(4); // Borrows mutably
}
let mut numbers = vec![1, 2, 3];
modify_vec(&mut numbers); // Pass mutable reference
println!("{:?}", numbers); // [1, 2, 3, 4]
Key Rules:
- Each value has one owner
- When owner goes out of scope, value is dropped
- You can have either one mutable reference OR multiple immutable references
- No reference can outlive its owner
2. Null Safety
Python (None can cause runtime errors):
def find_user(id: int) -> dict | None:
if id == 1:
return {"name": "Alice"}
return None
user = find_user(2)
print(user["name"]) # TypeError: 'NoneType' object is not subscriptable
Rust (Option type prevents null errors at compile time):
fn find_user(id: i32) -> Option<HashMap<String, String>> {
if id == 1 {
let mut user = HashMap::new();
user.insert("name".to_string(), "Alice".to_string());
Some(user)
} else {
None
}
}
let user = find_user(2);
// Compiler forces you to handle None case
match user {
Some(u) => println!("{}", u.get("name").unwrap()),
None => println!("User not found"),
}
3. String Types
Python (one string type):
s = "hello"
s = s + " world" # Creates new string
Rust (two main string types):
// &str: string slice (borrowed, immutable)
let s: &str = "hello";
// String: owned, growable
let mut s = String::from("hello");
s.push_str(" world");
When to use what:
&strfor string literals, function parameters (read-only)Stringwhen you need to own/modify the string
4. No Classes, Use Structs + Traits
Python (classes with inheritance):
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
Rust (composition over inheritance):
struct Animal {
name: String,
}
trait Speak {
fn speak(&self) -> String;
}
struct Dog {
animal: Animal,
}
impl Speak for Dog {
fn speak(&self) -> String {
format!("{} says Woof!", self.animal.name)
}
}
5. Iterators Are Lazy
Python (eager by default):
numbers = [1, 2, 3, 4, 5]
doubled = [x * 2 for x in numbers] # Executes immediately
Rust (lazy evaluation):
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers
.iter() // Creates iterator
.map(|x| x * 2) // Lazy: nothing happens yet
.collect(); // Consumes iterator, executes chain
Common Python Patterns in Rust
List Comprehensions → Iterator Chains
Python:
# Filter and map
evens_squared = [x**2 for x in range(10) if x % 2 == 0]
# Nested comprehension
matrix = [[i*j for j in range(3)] for i in range(3)]
Rust:
// Filter and map
let evens_squared: Vec<i32> = (0..10)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.collect();
// Nested iteration
let matrix: Vec<Vec<i32>> = (0..3)
.map(|i| (0..3).map(|j| i * j).collect())
.collect();
Dictionary Operations → HashMap
Python:
# Get with default
value = my_dict.get("key", "default")
# Iterate
for key, value in my_dict.items():
print(f"{key}: {value}")
# Dict comprehension
squares = {x: x**2 for x in range(5)}
Rust:
// Get with default
let value = my_map.get("key").unwrap_or(&"default");
// Iterate
for (key, value) in &my_map {
println!("{}: {}", key, value);
}
// Build from iterator
let squares: HashMap<i32, i32> = (0..5)
.map(|x| (x, x * x))
.collect();
Context Managers → Drop Trait
Python:
with open("file.txt", "r") as f:
content = f.read()
# File automatically closed
Rust:
use std::fs::File;
use std::io::Read;
let mut content = String::new();
{
let mut f = File::open("file.txt")?;
f.read_to_string(&mut content)?;
} // File automatically closed (Drop trait)
// Or use helper
let content = std::fs::read_to_string("file.txt")?;
Decorators → Procedural Macros
Python:
@cache
def expensive_function(x):
return x ** 2
Rust:
// Using derive macros (similar concept)
#[derive(Debug, Clone)]
struct MyStruct {
field: i32,
}
// Custom procedural macros are more complex
// but can achieve similar metaprogramming
Things That Are Easier in Rust
- Concurrency: No GIL, true parallelism
- Performance: 10-100x faster than Python
- Memory usage: No GC overhead
- Type safety: Catch errors at compile time
- Single binary: No dependency management at runtime
Things That Are Easier in Python
- Rapid prototyping: No compilation step
- Dynamic typing: Less boilerplate for scripts
- REPL: Interactive development
- Ecosystem: Massive library selection
- Learning curve: Gentler for beginners
Mental Model Shifts
From "Make it work" to "Make the compiler happy"
In Python, you run the code to find bugs. In Rust, the compiler finds bugs before you run.
Python approach:
def process(data):
result = None
if data:
result = data[0]
return result.upper() # AttributeError if data is empty (result is None)
Rust approach:
fn process(data: &[String]) -> Option<String> {
data.first() // Returns Option<&String>
.map(|s| s.to_uppercase()) // Compile error if not handled
}
// Compiler forces you to handle empty case
From "Duck typing" to "Explicit traits"
Python:
def print_all(items):
for item in items: # Works with any iterable
print(item)
Rust:
fn print_all<T: std::fmt::Display>(items: &[T]) {
for item in items { // T must implement Display
println!("{}", item);
}
}
Learning Path for Python Developers
Week 1: Core Syntax
- Chapter 1-2: Get familiar with basic syntax
- Practice: Rewrite simple Python scripts in Rust
- Focus: Variables, types, functions, control flow
Week 2: The Ownership Model
- Chapter 3: Deep dive into ownership
- This is the hardest concept - spend time here
- Practice: Understand when to use &, &mut, and owned values
Week 3: Structs and Error Handling
- Chapters 6, 8: Replace classes with structs
- Replace try/except with Result/Option
- Practice: Port a Python class-based program
Week 4: Collections and Iterators
- Chapter 7: HashMap, Vec, HashSet
- Replace list comprehensions with iterator chains
- Practice: Port data processing scripts
Week 5+: Advanced Features
- Traits (like interfaces)
- Concurrency (threads, channels)
- Async/await (similar to Python asyncio)
Common Pitfalls
1. Fighting the Borrow Checker
Bad (trying to use after move):
let s = String::from("hello");
let s2 = s; // s moved to s2
println!("{}", s); // ERROR: value used after move
Good (clone or borrow):
let s = String::from("hello");
let s2 = s.clone(); // Clone the data
println!("{} {}", s, s2); // Both valid
// Or just borrow
let s = String::from("hello");
let s2 = &s; // Borrow, don't move
println!("{} {}", s, s2);
2. Returning References
Bad:
fn first_word(s: &String) -> &str {
let word = String::from("hello");
&word // ERROR: returns reference to local variable
}
Good:
fn first_word(s: &str) -> String {
s.split_whitespace()
.next()
.unwrap_or("")
.to_string() // Return owned String
}
3. Over-cloning
Inefficient:
fn print_string(s: String) { // Takes ownership
println!("{}", s);
}
let s = String::from("hello");
print_string(s.clone()); // Unnecessary clone
print_string(s.clone()); // Another unnecessary clone
Better:
fn print_string(s: &str) { // Borrow instead
println!("{}", s);
}
let s = String::from("hello");
print_string(&s); // Just borrow
print_string(&s); // Can borrow multiple times
Helpful Tips
- Read compiler errors carefully: They're educational, not just error messages
- Use
cargo clippy: It suggests idiomatic Rust patterns - Start with
clone(): Optimize later once you understand ownership - When in doubt, borrow: Use
&instead of taking ownership - Write Python pseudocode first: Then translate to Rust
- Use the playground: play.rust-lang.org for quick tests (external resource)
Equivalent Rust Crates for Python Libraries
| Python | Rust | Purpose |
|---|---|---|
| requests | reqwest | HTTP client |
| flask/fastapi | actix-web, axum | Web frameworks |
| pandas | polars | Data analysis |
| numpy | ndarray | Numerical computing |
| pytest | built-in cargo test | Testing |
| click | clap | CLI argument parsing |
| asyncio | tokio, async-std | Async runtime |
| json | serde_json | JSON serialization |
| regex | regex | Regular expressions |
Practice Project Ideas
Start with familiar Python projects:
- CLI Tool: Port a Python command-line tool
- Web Scraper: Use reqwest + scraper crates
- REST API: Build with actix-web or axum
- Data Processor: CSV/JSON manipulation with serde
- File Organizer: File system operations
Resources for Python Developers
- The Rust Book - Official comprehensive guide
- Rust by Example - Learn by doing
- Rustlings - Small exercises
Note: External resources may change over time. Check repository READMEs for current status.
Summary
Rust will feel very different from Python initially. The compiler is strict, but it's teaching you to write safe, fast code. Give yourself time to adjust to:
- Ownership system - Hardest concept, but most important
- Static typing - More upfront work, fewer runtime bugs
- Explicit error handling - No exceptions, use Result/Option
- Immutability by default - More functional style
- No null - Option type instead
The learning curve is steep, but the payoff is substantial: memory-safe, concurrent programs that run as fast as C/C++.
Next Steps
Continue to Chapter 1: Introduction for detailed setup and your first Rust programs, now with your Python knowledge providing context for the differences you'll encounter.