Python's Memory Management
In Python, you rarely think about memory management. The garbage collector automatically cleans up objects when they're no longer referenced:
Memory Management Comparison
# Python: Automatic memory management
def create_data():
data = [1, 2, 3, 4, 5] # Memory allocated
return data # Reference returned
my_data = create_data() # data lives on
# When my_data goes out of scope or is reassigned,
# garbage collector will eventually clean it up
// Rust: Explicit ownership
fn create_data() -> Vec<i32> {
let data = vec![1, 2, 3, 4, 5]; // Memory allocated
data // Ownership transferred to caller
}
fn main() {
let my_data = create_data(); // my_data owns the Vec
// When my_data goes out of scope, memory is freed immediately
}
The Three Rules of Ownership
Rust's ownership system is built on three simple rules:
- Each value in Rust has a variable that's called its owner
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped
Ownership Transfer (Move)
In Python, assignment typically creates a new reference to the same object. In Rust, assignment can transfer ownership:
Assignment Behavior
# Python: Multiple references to same object
original_list = [1, 2, 3]
copied_list = original_list # Both point to same object
original_list.append(4)
print(copied_list) # [1, 2, 3, 4] - both see the change
# To actually copy:
import copy
real_copy = copy.deepcopy(original_list)
// Rust: Ownership transfer (move)
let original_vec = vec![1, 2, 3];
let moved_vec = original_vec; // Ownership transferred
// println!("{:?}", original_vec); // ❌ Error! original_vec no longer valid
println!("{:?}", moved_vec); // ✅ Works fine
// To actually copy:
let original_vec = vec![1, 2, 3];
let copied_vec = original_vec.clone(); // Explicit copy
Borrowing: Temporary Access
Sometimes you want to use a value without taking ownership. Rust provides "borrowing" through references, similar to how you might pass objects to functions in Python:
Borrowing vs Direct Access
# Python: Passing objects to functions
def print_length(data):
print(f"Length: {len(data)}")
# data is still accessible in the caller
def modify_list(data):
data.append(999) # Modifies original list
my_list = [1, 2, 3]
print_length(my_list) # my_list still usable
modify_list(my_list) # my_list is modified
print(my_list) # [1, 2, 3, 999]
// Rust: Borrowing with references
fn print_length(data: &Vec<i32>) { // Immutable borrow
println!("Length: {}", data.len());
}
fn modify_vec(data: &mut Vec<i32>) { // Mutable borrow
data.push(999);
}
fn main() {
let mut my_vec = vec![1, 2, 3];
print_length(&my_vec); // Borrow, my_vec still owned here
modify_vec(&mut my_vec); // Mutable borrow
println!("{:?}", my_vec); // [1, 2, 3, 999]
}
The Borrow Checker
Rust's borrow checker enforces rules that prevent common bugs like use-after-free and data races. These rules might seem strict coming from Python:
Borrow Checker Rules
# Python: This can lead to issues
data = [1, 2, 3]
reference1 = data
reference2 = data
# Both can modify simultaneously
reference1.append(4)
reference2.append(5)
# In multithreaded code, this could cause race conditions
// Rust: Borrow checker prevents issues
let mut data = vec![1, 2, 3];
let ref1 = &mut data; // Mutable borrow
// let ref2 = &data; // ❌ Error! Can't have immutable borrow while mutable exists
ref1.push(4);
// Only one mutable reference allowed at a time
// This prevents data races at compile time!
Smart Pointers: When You Need Flexibility
Sometimes ownership rules are too restrictive. Rust provides smart pointers that offer more flexibility, similar to Python's natural behavior:
Shared Ownership with Smart Pointers
# Python: Shared ownership is natural
import threading
class SharedData:
def __init__(self):
self.value = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
self.value += 1
# Multiple threads can share the same object
shared = SharedData()
# Pass shared to multiple threads...
// Rust: Shared ownership with smart pointers
use std::sync::{Arc, Mutex};
use std::thread;
#[derive(Debug)]
struct SharedData {
value: i32,
}
fn main() {
let shared = Arc::new(Mutex::new(SharedData { value: 0 }));
let handles: Vec<_> = (0..3).map(|_| {
let shared_clone = Arc::clone(&shared);
thread::spawn(move || {
let mut data = shared_clone.lock().unwrap();
data.value += 1;
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
}
Key Takeaways
- Ownership prevents bugs: No use-after-free, no data races
- Zero-cost abstractions: Safety without runtime overhead
- Explicit is better: Memory management is visible and predictable
- Borrowing is lending: Use references when you don't need ownership
- Smart pointers for flexibility: When ownership rules are too strict
Think of it this way:
- Python: "Everything is a reference, garbage collector cleans up"
- Rust: "Everything has an owner, cleanup happens when owner goes away"
- Python: "Pass objects around freely"
- Rust: "Lend with &, give away with move, copy with .clone()"
The ownership system might feel restrictive at first, but it prevents entire classes of bugs that can occur in Python (especially in concurrent code). The compiler becomes your pair programming partner, catching issues before they reach production.