Back to Tutorials

Ownership for Pythonistas

12 min readIntermediate

Rust's ownership system is its most distinctive feature. As a Python developer, you're used to garbage collection handling memory for you. Let's explore how Rust's approach differs and why it's so powerful.

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
# 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
// 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:

  1. Each value in Rust has a variable that's called its owner
  2. There can only be one owner at a time
  3. 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
# 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
// 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
# 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
// 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
# 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
// 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
# 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
// 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.