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

AspectPythonRust
Type SystemDynamic, duck typingStatic, explicit types
Memory ManagementGarbage collectionOwnership system (no GC)
MutabilityMutable by defaultImmutable by default
Error HandlingExceptionsResult/Option types
CompilationInterpretedCompiled to native code
PerformanceSlower, optimized for dev speedFast, zero-cost abstractions
ConcurrencyGIL limits true parallelismFearless 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:

  • &str for string literals, function parameters (read-only)
  • String when 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

  1. Concurrency: No GIL, true parallelism
  2. Performance: 10-100x faster than Python
  3. Memory usage: No GC overhead
  4. Type safety: Catch errors at compile time
  5. Single binary: No dependency management at runtime

Things That Are Easier in Python

  1. Rapid prototyping: No compilation step
  2. Dynamic typing: Less boilerplate for scripts
  3. REPL: Interactive development
  4. Ecosystem: Massive library selection
  5. 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

  1. Chapter 1-2: Get familiar with basic syntax
  2. Practice: Rewrite simple Python scripts in Rust
  3. Focus: Variables, types, functions, control flow

Week 2: The Ownership Model

  1. Chapter 3: Deep dive into ownership
  2. This is the hardest concept - spend time here
  3. Practice: Understand when to use &, &mut, and owned values

Week 3: Structs and Error Handling

  1. Chapters 6, 8: Replace classes with structs
  2. Replace try/except with Result/Option
  3. Practice: Port a Python class-based program

Week 4: Collections and Iterators

  1. Chapter 7: HashMap, Vec, HashSet
  2. Replace list comprehensions with iterator chains
  3. Practice: Port data processing scripts

Week 5+: Advanced Features

  1. Traits (like interfaces)
  2. Concurrency (threads, channels)
  3. 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

  1. Read compiler errors carefully: They're educational, not just error messages
  2. Use cargo clippy: It suggests idiomatic Rust patterns
  3. Start with clone(): Optimize later once you understand ownership
  4. When in doubt, borrow: Use & instead of taking ownership
  5. Write Python pseudocode first: Then translate to Rust
  6. Use the playground: play.rust-lang.org for quick tests (external resource)

Equivalent Rust Crates for Python Libraries

PythonRustPurpose
requestsreqwestHTTP client
flask/fastapiactix-web, axumWeb frameworks
pandaspolarsData analysis
numpyndarrayNumerical computing
pytestbuilt-in cargo testTesting
clickclapCLI argument parsing
asynciotokio, async-stdAsync runtime
jsonserde_jsonJSON serialization
regexregexRegular expressions

Practice Project Ideas

Start with familiar Python projects:

  1. CLI Tool: Port a Python command-line tool
  2. Web Scraper: Use reqwest + scraper crates
  3. REST API: Build with actix-web or axum
  4. Data Processor: CSV/JSON manipulation with serde
  5. File Organizer: File system operations

Resources for Python Developers

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:

  1. Ownership system - Hardest concept, but most important
  2. Static typing - More upfront work, fewer runtime bugs
  3. Explicit error handling - No exceptions, use Result/Option
  4. Immutability by default - More functional style
  5. 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.