PONY λ M2 Modula-2

Python.CodeCompared.To/Rust

An interactive executable cheatsheet comparing Python and Rust

Python 3.13 Rust 1.95
Output & Running
Hello, World
print("Hello, World!")
println!("Hello, World!");
println! is a macro (note the !), not a function. Like Python's print(), it appends a trailing newline. Every standalone Rust program requires a fn main() entry point — the runner adds this boilerplate automatically.
Printing multiple values
name = "Alice" age = 30 print(name, age) # space-separated print(f"{name} is {age}") # f-string
let name = "Alice"; let age = 30; println!("{} {}", name, age); // positional placeholders println!("{name} is {age}"); // captured variable names (Rust 1.58+)
Rust's println! uses {} placeholders filled left-to-right from its arguments, similar to Python's str.format(). Since Rust 1.58, you can capture variable names directly inside {}, matching Python f-string syntax.
Debug output
coordinates = (1, 2, 3) numbers = [1, 2, 3] print(repr(coordinates)) print(repr(numbers))
let coordinates = (1, 2, 3); let numbers = vec![1, 2, 3]; println!("{:?}", coordinates); println!("{:?}", numbers);
{:?} invokes the Debug trait, which produces machine-readable output similar to Python's repr(). Use {:#?} for pretty-printed (indented) debug output.
Formatted strings
pi = 3.14159 label = "pi" print(f"{label} = {pi:.2f}") result = f"Result: {pi:.4f}" print(result)
let pi = 3.14159_f64; let label = "pi"; println!("{label} = {pi:.2}"); let result = format!("Result: {pi:.4}"); println!("{result}");
The format! macro builds a String without printing it, analogous to Python's f-string used for assignment. Rust format specifiers use :.2 (digits after the decimal), matching Python's :.2f convention.
Comments
# single-line comment """ Multi-line docstring / comment """ x = 42 # inline comment
// single-line comment /* Multi-line comment */ let x = 42; // inline comment
Rust uses // for line comments and /* */ for block comments, matching C-style conventions. Python's triple-quoted strings are sometimes used as multi-line comments, but Rust has dedicated block comment syntax.
Variables & Types
Immutable binding
greeting = "hello" count = 42 print(greeting, count)
let greeting = "hello"; let count = 42; println!("{greeting} {count}");
In Rust, let bindings are immutable by default — reassigning them is a compile error. Python variables are always mutable references; Rust enforces immutability at the language level.
Mutable variable
counter = 0 counter += 1 counter += 1 print(counter)
let mut counter = 0; counter += 1; counter += 1; println!("{counter}");
Mutation requires the explicit mut keyword. This makes it immediately visible at the declaration site whether a variable is intended to change — a form of documentation that Python lacks.
Type inference
x = 42 # int inferred y = 3.14 # float inferred z = "hello" # str inferred print(type(x), type(y), type(z))
let x = 42; // i32 inferred let y = 3.14; // f64 inferred let z = "hello"; // &str inferred println!("{x} {y} {z}");
Rust infers types at compile time — you rarely need to annotate locals. Unlike Python's dynamic typing, Rust's types are fixed at compile time; the compiler catches type mismatches before any code runs.
Explicit type annotation
score: int = 100 ratio: float = 0.75 name: str = "Alice" print(score, ratio, name)
let score: i32 = 100; let ratio: f64 = 0.75; let name: &str = "Alice"; println!("{score} {ratio} {name}");
Rust has many integer types (i8, i16, i32, i64, i128, u8u128, usize) and two float types (f32, f64). Python has a single unbounded int and float.
Variable shadowing
value = "42" value = int(value) # reassigns the name value = value * 2 print(value)
let value = "42"; let value = value.parse::<i32>().unwrap(); // shadows previous binding let value = value * 2; println!("{value}");
Rust allows re-declaring a variable with the same name via let, which creates a new binding that shadows the old one. Shadowing can even change the type, whereas Python reassignment just rebinds the same name.
Constants
MAX_CONNECTIONS = 100 # convention: SCREAMING_SNAKE_CASE PI = 3.14159 print(MAX_CONNECTIONS, PI)
const MAX_CONNECTIONS: u32 = 100; const PI: f64 = 3.14159; fn main() { println!("{MAX_CONNECTIONS} {PI}"); }
Rust const requires an explicit type annotation and must be a compile-time value. Unlike Python's uppercase convention, Rust's const is enforced by the compiler — the value is inlined everywhere it is used.
Booleans
active = True inactive = False print(active and not inactive) print(active or inactive)
let active = true; let inactive = false; println!("{}", active && !inactive); println!("{}", active || inactive);
Rust booleans are lowercase true/false. Rust uses &&, ||, and ! for logical operators; Python uses the English keywords and, or, not.
Strings
&str vs String
# Python has one string type literal = "hello" # str literal dynamic = "hello".upper() # str print(type(literal), type(dynamic))
let literal: &str = "hello"; // string slice — borrowed, immutable let dynamic: String = String::from("hello"); // heap-allocated, owned let also: String = "hello".to_string(); println!("{literal} {dynamic} {also}");
Rust has two string types: &str (a borrowed reference to UTF-8 bytes, often a string literal) and String (an owned, heap-allocated, growable string). Python has only one str type.
String concatenation
first = "Hello" second = " World" combined = first + second print(combined)
let first = String::from("Hello"); let second = " World"; let combined = first + second; // first is moved here println!("{combined}");
The + operator on a String takes ownership of the left operand and appends the right (&str). After first + second, first is no longer usable. For repeated concatenation, prefer format! to avoid confusion.
Common string methods
text = " Hello, World! " print(text.strip()) print(text.strip().upper()) print(text.strip().lower()) print(text.strip().replace("World", "Rust"))
let text = " Hello, World! "; println!("{}", text.trim()); println!("{}", text.trim().to_uppercase()); println!("{}", text.trim().to_lowercase()); println!("{}", text.trim().replace("World", "Rust"));
Rust's string methods mirror Python's: trim() replaces strip(), to_uppercase() replaces upper(). Methods return new values — strings in Rust are immutable by default.
Split and join
sentence = "one two three" words = sentence.split() print(words) joined = ", ".join(words) print(joined)
let sentence = "one two three"; let words: Vec<&str> = sentence.split_whitespace().collect(); println!("{:?}", words); let joined = words.join(", "); println!("{joined}");
Rust's split_whitespace() is a lazy iterator; calling .collect() materializes it into a Vec. Python's split() returns a list directly. Rust's join() is a method on slices, while Python's is on the separator string.
Contains / starts with / ends with
text = "Hello, World!" print("World" in text) print(text.startswith("Hello")) print(text.endswith("!"))
let text = "Hello, World!"; println!("{}", text.contains("World")); println!("{}", text.starts_with("Hello")); println!("{}", text.ends_with('!'));
Rust uses method calls where Python uses the in operator and methods. ends_with accepts both a &str and a char — note the single quotes for the char literal '!'.
Characters and Unicode
text = "café" print(len(text)) # 4 chars for character in text: print(character)
let text = "café"; println!("{}", text.chars().count()); // 4 Unicode scalar values for character in text.chars() { println!("{character}"); }
Rust strings are UTF-8 encoded bytes. text.len() returns bytes, not characters. Use text.chars().count() for character count, matching Python's len(). Rust's char type is a Unicode scalar value (like Python's single-character string).
Numbers
Integer types
small = 127 # Python int is arbitrary precision big = 10 ** 20 negative = -42 print(small, big, negative)
let small: i8 = 127; let medium: i32 = 2_147_483_647; let large: i64 = 9_223_372_036_854_775_807; let unsigned: u32 = 4_294_967_295; println!("{small} {medium} {large} {unsigned}");
Python integers have arbitrary precision and never overflow. Rust integers have fixed sizes; overflow panics in debug mode and wraps in release mode. Use underscores in numeric literals for readability — both Python and Rust support this.
Integer arithmetic
print(10 + 3) # 13 print(10 - 3) # 7 print(10 * 3) # 30 print(10 // 3) # 3 (integer division) print(10 % 3) # 1 (remainder) print(2 ** 10) # 1024 (exponentiation)
println!("{}", 10 + 3); // 13 println!("{}", 10 - 3); // 7 println!("{}", 10 * 3); // 30 println!("{}", 10 / 3); // 3 (integer division when both are integers) println!("{}", 10 % 3); // 1 (remainder) println!("{}", i32::pow(2, 10)); // 1024
In Rust, dividing two integers does integer division automatically — there is no // operator. Python's / always returns a float; Rust's / returns the same type as its operands. Rust has no ** exponentiation operator; use i32::pow() or f64::powi().
Float arithmetic
print(10 / 3) # 3.3333... print(round(3.14159, 2)) import math print(math.sqrt(16.0)) print(math.pi)
println!("{}", 10.0_f64 / 3.0); println!("{:.2}", 3.14159_f64); println!("{}", 16.0_f64.sqrt()); println!("{}", std::f64::consts::PI);
Rust requires at least one operand to be a float literal for float division. Mathematical constants live in std::f64::consts. Formatting with {:.2} rounds for display (like Python's round() used in f-strings).
Numeric type conversion
x = 42 y = float(x) # int → float z = int(3.9) # float → int (truncates) s = str(x) # int → str n = int("123") # str → int print(y, z, s, n)
let x: i32 = 42; let y: f64 = x as f64; // int → float let z: i32 = 3.9_f64 as i32; // float → int (truncates) let s: String = x.to_string(); // int → String let n: i32 = "123".parse().unwrap(); // str → int println!("{y} {z} {s} {n}");
Rust requires explicit casts with as for numeric conversions — implicit widening does not exist. Parsing a string returns a Result; .unwrap() panics on failure. Python's constructors int() and float() raise ValueError on failure instead.
Ownership & Borrowing
Ownership basics
# Python: reference-counted garbage collection name = "Alice" alias = name # both refer to same object print(name, alias) # both still usable
let name = String::from("Alice"); let alias = name; // name is MOVED here — ownership transferred // println!("{name}"); // compile error: name was moved println!("{alias}");
Rust's ownership model means each value has exactly one owner at a time. Assigning a heap value (String) to another variable moves it — the original is no longer usable. Python uses reference counting and garbage collection, so aliases are always safe.
Cloning (deep copy)
import copy original = [1, 2, 3] duplicate = copy.deepcopy(original) duplicate.append(4) print(original) # [1, 2, 3] — unchanged print(duplicate) # [1, 2, 3, 4]
let original = vec![1, 2, 3]; let mut duplicate = original.clone(); duplicate.push(4); println!("{:?}", original); // [1, 2, 3] — unchanged println!("{:?}", duplicate); // [1, 2, 3, 4]
.clone() creates a deep copy of a value, allowing both the original and the copy to coexist as independent owners. This is analogous to Python's copy.deepcopy(). Cloning is explicit in Rust — the compiler never clones silently.
Borrowing (&T)
def print_length(items): print(len(items)) # borrows but doesn't consume numbers = [1, 2, 3] print_length(numbers) print(numbers) # still usable
fn print_length(items: &Vec<i32>) { println!("{}", items.len()); // borrows — does not take ownership } fn main() { let numbers = vec![1, 2, 3]; print_length(&numbers); println!("{:?}", numbers); // still usable }
Passing &numbers lends the value to the function without transferring ownership. The function receives a shared reference (&Vec) and may read but not modify the data. Python functions always receive references implicitly — there is no equivalent opt-in.
Mutable borrowing (&mut T)
def append_item(items, item): items.append(item) # mutates the caller's list numbers = [1, 2, 3] append_item(numbers, 4) print(numbers)
fn append_item(items: &mut Vec<i32>, item: i32) { items.push(item); // mutates through mutable reference } fn main() { let mut numbers = vec![1, 2, 3]; append_item(&mut numbers, 4); println!("{:?}", numbers); }
A mutable reference (&mut T) allows a function to modify the caller's data. Rust enforces that only one mutable reference to a value can exist at a time — this eliminates data races at compile time. Python has no such restriction.
Copy types (stack values)
# Python: integers are immutable; re-binding is safe x = 42 y = x # both refer to equal-but-independent values y += 1 print(x, y) # 42, 43
let x = 42_i32; let y = x; // i32 implements Copy — x is copied, not moved let y = y + 1; println!("{x} {y}"); // 42 43 — both usable
Simple types like integers, booleans, and chars implement the Copy trait, meaning assignment copies their value on the stack instead of moving ownership. Heap types like String and Vec do not implement Copy and are moved instead.
Collections
Vector (list equivalent)
numbers = [1, 2, 3, 4, 5] numbers.append(6) print(numbers[0]) # first element print(numbers[-1]) # last element print(len(numbers))
let mut numbers = vec![1, 2, 3, 4, 5]; numbers.push(6); println!("{}", numbers[0]); // first element println!("{}", numbers[numbers.len() - 1]); // last element println!("{}", numbers.len());
Vec is Rust's growable list, equivalent to Python's list. The vec! macro creates a vector with initial values. Negative indexing does not exist in Rust — use .last() or compute the index.
Slicing
numbers = [1, 2, 3, 4, 5] print(numbers[1:4]) # [2, 3, 4] print(numbers[:2]) # [1, 2] print(numbers[2:]) # [3, 4, 5]
let numbers = vec![1, 2, 3, 4, 5]; println!("{:?}", &numbers[1..4]); // [2, 3, 4] println!("{:?}", &numbers[..2]); // [1, 2] println!("{:?}", &numbers[2..]); // [3, 4, 5]
Rust slice syntax uses .. ranges (exclusive end) instead of Python's : syntax. The & prefix borrows the slice without copying. Rust ranges also support inclusive end with ..=.
HashMap (dict equivalent)
ages = {"Alice": 30, "Bob": 25} ages["Carol"] = 28 print(ages["Alice"]) print(ages.get("Dave", 0)) # default value print(len(ages))
use std::collections::HashMap; let mut ages = HashMap::from([("Alice", 30), ("Bob", 25)]); ages.insert("Carol", 28); println!("{}", ages["Alice"]); println!("{}", ages.get("Dave").unwrap_or(&0)); println!("{}", ages.len());
HashMap must be imported from std::collections. Indexing with [] panics if the key is missing; use .get() for safe access, which returns Option<&V>. Python's dict.get(key, default) maps to .get(key).unwrap_or(&default).
HashSet (set equivalent)
unique = {1, 2, 3, 2, 1} print(unique) # {1, 2, 3} unique.add(4) print(3 in unique)
use std::collections::HashSet; let mut unique: HashSet<i32> = HashSet::from([1, 2, 3, 2, 1]); println!("{:?}", unique); // {1, 2, 3} (order varies) unique.insert(4); println!("{}", unique.contains(&3));
HashSet stores unique values with O(1) lookup, like Python's set. Elements can be collected from any iterator with .collect(). Both Python and Rust HashSets are unordered.
Tuples
point = (10, 20) triple = (1, "hello", True) x, y = point # destructuring print(point[0], point[1]) print(x, y)
let point = (10, 20); let triple = (1, "hello", true); let (x, y) = point; // destructuring println!("{} {}", point.0, point.1); println!("{x} {y}");
Rust tuples are indexed with .0, .1, etc., rather than [0], [1]. Like Python, they support destructuring assignment. Rust tuple types are part of the type system: (i32, i32) and (i32, &str, bool) are distinct types.
Const generics
def total(values): return sum(values) # Python lists are dynamic; length is never part of a "type". print(total([1, 2, 3])) print(total([1, 2, 3, 4, 5]))
// N is a compile-time constant: each array length is its own type, // yet one definition covers them all. fn total<const N: usize>(values: [i32; N]) -> i32 { values.iter().sum() } fn main() { println!("{}", total([1, 2, 3])); println!("{}", total([1, 2, 3, 4, 5])); }
Const generics let a definition be parameterized over a value — here the array length N — not just over types. Python has no equivalent: every list is dynamically sized and length is purely a runtime property. Because the length is part of the Rust type and fixed at compile time, [i32; 3] and [i32; 5] are distinct types served by one total, stored inline on the stack with no heap allocation.
Control Flow
if / else if / else
score = 85 if score >= 90: print("A") elif score >= 80: print("B") else: print("C or below")
let score = 85; if score >= 90 { println!("A"); } else if score >= 80 { println!("B"); } else { println!("C or below"); }
Rust uses else if (not elif) and requires curly braces around every block. Parentheses around the condition are optional and conventionally omitted. Indentation is not syntactically significant in Rust.
if as an expression
score = 85 grade = "pass" if score >= 60 else "fail" print(grade)
let score = 85; let grade = if score >= 60 { "pass" } else { "fail" }; println!("{grade}");
In Rust, if is an expression that returns the last value in the chosen branch (with no semicolon on that line). Python's ternary x if cond else y maps naturally to Rust's if cond { x } else { y }.
match (switch equivalent)
direction = "north" match direction: case "north": print("Going north") case "south": print("Going south") case _: print("Unknown direction")
let direction = "north"; match direction { "north" => println!("Going north"), "south" => println!("Going south"), _ => println!("Unknown direction"), }
Rust's match is exhaustive — the compiler requires all possible values to be handled (use _ as a catch-all). Python's match (added in 3.10) is similar but not enforced by the interpreter.
match with guards
temperature = 25 match temperature: case t if t < 0: print("freezing") case t if t < 20: print("cold") case t if t < 30: print("comfortable") case _: print("hot")
let temperature = 25; match temperature { t if t < 0 => println!("freezing"), t if t < 20 => println!("cold"), t if t < 30 => println!("comfortable"), _ => println!("hot"), }
Both Python and Rust support guard conditions in match arms using if. Rust match arms use => to separate the pattern from the body. Multiple comma-separated patterns can share one arm: "a" | "b" => ....
match on ranges
score = 85 match score: case s if 90 <= s <= 100: grade = "A" case s if 80 <= s < 90: grade = "B" case s if 70 <= s < 80: grade = "C" case _: grade = "F" print(grade)
let score = 85; let grade = match score { 90..=100 => "A", 80..=89 => "B", 70..=79 => "C", _ => "F", }; println!("{grade}");
Rust match supports range patterns (80..=89 means 80 through 89 inclusive). This is more concise than Python's guard-based approach. Range patterns use ..= (inclusive) — exclusive ranges are not valid in match patterns.
Loops & Iterators
for over a range
for i in range(5): print(i) for i in range(1, 6): print(i)
for i in 0..5 { println!("{i}"); } for i in 1..=5 { println!("{i}"); }
Rust's range 0..5 is exclusive (like Python's range(5)) and 1..=5 is inclusive. Ranges are iterators — they work directly in for loops without a separate range() call.
Iterating a collection
fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit)
let fruits = vec!["apple", "banana", "cherry"]; for fruit in &fruits { println!("{fruit}"); }
The &fruits borrows the vector for iteration, leaving the original usable afterward. Iterating without & would move the vector and consume it. Python's for loop always borrows implicitly.
Enumerate (index + value)
colors = ["red", "green", "blue"] for index, color in enumerate(colors): print(f"{index}: {color}")
let colors = vec!["red", "green", "blue"]; for (index, color) in colors.iter().enumerate() { println!("{index}: {color}"); }
Rust's .enumerate() is an iterator adapter, chained after .iter(). It yields (usize, &T) tuples. Python's enumerate() is a built-in that wraps any iterable.
while loop
count = 0 while count < 5: print(count) count += 1
let mut count = 0; while count < 5 { println!("{count}"); count += 1; }
Rust's while loop is identical in structure to Python's. Note that count must be declared mut to allow mutation. Rust has no while True: — use loop { } for an infinite loop.
Infinite loop with break
count = 0 while True: if count >= 3: break print(count) count += 1
let mut count = 0; loop { if count >= 3 { break; } println!("{count}"); count += 1; }
Rust's loop keyword creates an unconditional infinite loop, replacing Python's while True:. A loop block can also return a value: let result = loop { break 42; };.
map / filter / collect
numbers = [1, 2, 3, 4, 5] doubled = list(map(lambda n: n * 2, numbers)) evens = list(filter(lambda n: n % 2 == 0, numbers)) print(doubled) print(evens)
let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect(); let evens: Vec<i32> = numbers.iter().filter(|n| *n % 2 == 0).cloned().collect(); println!("{:?}", doubled); println!("{:?}", evens);
Rust iterators are lazy — nothing runs until you call a consuming adapter like .collect(), .sum(), or .count(). This matches Python's map() and filter() which also return lazy iterators (wrapped with list() to evaluate).
sum / chain / zip
numbers = [1, 2, 3, 4, 5] print(sum(numbers)) first = [1, 2, 3] second = [4, 5, 6] combined = list(first) + list(second) print(combined)
let numbers = vec![1, 2, 3, 4, 5]; println!("{}", numbers.iter().sum::<i32>()); let first = vec![1, 2, 3]; let second = vec![4, 5, 6]; let combined: Vec<i32> = first.iter().chain(second.iter()).cloned().collect(); println!("{:?}", combined);
.sum() requires a type annotation to resolve ambiguity. .chain() lazily concatenates two iterators without allocating an intermediate collection. Python's + on lists creates a new list eagerly.
Functions
Basic function
def greet(name): print(f"Hello, {name}!") greet("Alice")
fn greet(name: &str) { println!("Hello, {name}!"); } fn main() { greet("Alice"); }
Rust function parameters must have explicit type annotations — there is no type inference for function signatures. The return type (if any) is declared after ->. Functions without an explicit return type implicitly return the unit type ().
Return values
def add(x, y): return x + y def square(n): return n * n print(add(3, 4)) print(square(5))
fn add(x: i32, y: i32) -> i32 { x + y // implicit return — last expression without semicolon } fn square(n: i32) -> i32 { n * n } fn main() { println!("{}", add(3, 4)); println!("{}", square(5)); }
In Rust, omitting the semicolon from the last expression returns it implicitly. This is idiomatic Rust. You can also use return explicitly for early returns. Python always uses the explicit return keyword.
Multiple return values
def min_max(numbers): return min(numbers), max(numbers) low, high = min_max([3, 1, 4, 1, 5]) print(low, high)
fn min_max(numbers: &[i32]) -> (i32, i32) { (*numbers.iter().min().unwrap(), *numbers.iter().max().unwrap()) } fn main() { let (low, high) = min_max(&[3, 1, 4, 1, 5]); println!("{low} {high}"); }
Rust returns multiple values by returning a tuple, just like Python. The caller destructures it with let (a, b) = .... The &[i32] parameter type is a slice — a view into any contiguous sequence of i32 values.
Type annotations (Python) vs required types (Rust)
def multiply(x: int, y: int) -> int: return x * y print(multiply(6, 7))
fn multiply(x: i32, y: i32) -> i32 { x * y } fn main() { println!("{}", multiply(6, 7)); }
Python type hints are optional and ignored at runtime — the interpreter does not enforce them. Rust type annotations on function signatures are mandatory and enforced at compile time. Calling multiply("a", "b") would fail at runtime in Python but is a compile error in Rust.
async / await
import asyncio async def double(value): await asyncio.sleep(0) return value * 2 async def main(): first = await double(21) print(first) asyncio.run(main())
use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; // std has async/await syntax but ships no executor, so here is a tiny one. // Real programs reach for tokio or async-std instead of hand-rolling this. fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let mut context = Context::from_waker(Waker::noop()); loop { if let Poll::Ready(value) = future.as_mut().poll(&mut context) { return value; } } } async fn double(value: i32) -> i32 { value * 2 } fn main() { let result = block_on(async { let first = double(21).await; first }); println!("{result}"); }
Both languages spell asynchronous code with async and await, and in both an async function is lazy — calling it produces a coroutine/future that does nothing until it is driven. The difference is the runtime: Python bundles an event loop you start with asyncio.run(), whereas Rust's std ships none, so production code adds a runtime such as tokio. The little block_on here is the moral equivalent of asyncio.run — it polls a single future to completion on the current thread.
Closures
Basic closure / lambda
double = lambda n: n * 2 print(double(5)) add = lambda x, y: x + y print(add(3, 4))
let double = |n| n * 2; println!("{}", double(5)); let add = |x, y| x + y; println!("{}", add(3, 4));
Rust closures use |params| body syntax. Type annotations are usually inferred from usage. Unlike Python lambdas (limited to a single expression), Rust closures can contain full block bodies with |params| { ... }.
Capturing the environment
prefix = "Hello" greet = lambda name: f"{prefix}, {name}!" print(greet("Alice"))
let prefix = "Hello"; let greet = |name: &str| format!("{prefix}, {name}!"); println!("{}", greet("Alice"));
Rust closures capture their environment automatically, but the compiler determines how — by shared reference, mutable reference, or move. Use move |...| to force ownership transfer into the closure (needed when the closure outlives the enclosing scope).
Higher-order functions
def apply(func, value): return func(value) result = apply(lambda n: n * n, 5) print(result)
fn apply<F: Fn(i32) -> i32>(func: F, value: i32) -> i32 { func(value) } fn main() { let result = apply(|n| n * n, 5); println!("{result}"); }
Higher-order functions in Rust use generic type parameters with trait bounds: F: Fn(i32) -> i32 accepts any callable matching that signature. Fn, FnMut, and FnOnce correspond to closures that borrow, mutably borrow, or consume their captured environment.
Sort with a key function
words = ["banana", "apple", "cherry", "date"] words.sort(key=lambda word: len(word)) print(words)
let mut words = vec!["banana", "apple", "cherry", "date"]; words.sort_by_key(|word| word.len()); println!("{:?}", words);
Rust's sort_by_key() takes a closure that maps each element to a sortable key, directly equivalent to Python's key= parameter. For more complex comparisons, use sort_by(|a, b| a.cmp(b)).
Structs & Methods
Defining a struct
class Point: def __init__(self, x, y): self.x = x self.y = y origin = Point(0, 0) print(origin.x, origin.y)
struct Point { x: f64, y: f64, } fn main() { let origin = Point { x: 0.0, y: 0.0 }; println!("{} {}", origin.x, origin.y); }
A Rust struct is a named collection of fields with explicit types — similar to a Python class with only __init__ and data attributes. Structs have no inheritance. Methods are added separately in an impl block.
Methods (impl block)
class Circle: def __init__(self, radius): self.radius = radius def area(self): import math return math.pi * self.radius ** 2 def describe(self): print(f"Circle with radius {self.radius}") circle = Circle(5.0) circle.describe() print(circle.area())
struct Circle { radius: f64, } impl Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn describe(&self) { println!("Circle with radius {}", self.radius); } } fn main() { let circle = Circle { radius: 5.0 }; circle.describe(); println!("{}", circle.area()); }
Methods are defined in an impl block separate from the struct definition. The first parameter &self is an immutable reference to the instance — equivalent to Python's self. Use &mut self if the method needs to modify the struct.
Constructor pattern (new)
class Rectangle: def __init__(self, width, height): self.width = width self.height = height @classmethod def square(cls, size): return cls(size, size) rect = Rectangle(4.0, 6.0) square = Rectangle.square(5.0) print(rect.width, rect.height) print(square.width, square.height)
struct Rectangle { width: f64, height: f64, } impl Rectangle { fn new(width: f64, height: f64) -> Rectangle { Rectangle { width, height } } fn square(size: f64) -> Rectangle { Rectangle { width: size, height: size } } } fn main() { let rect = Rectangle::new(4.0, 6.0); let square = Rectangle::square(5.0); println!("{} {}", rect.width, rect.height); println!("{} {}", square.width, square.height); }
Rust has no built-in constructors — the convention is to define an associated function named new (no self parameter, called with ::). This mirrors Python's __init__ and @classmethod. The shorthand Rectangle { width, height } uses field init shorthand when variable names match field names.
Enums & Pattern Matching
Enum definition
from enum import Enum class Direction(Enum): NORTH = "north" SOUTH = "south" EAST = "east" WEST = "west" current = Direction.NORTH print(current)
#[derive(Debug)] enum Direction { North, South, East, West, } fn main() { let current = Direction::North; println!("{:?}", current); }
Rust enums are used for any type with a fixed set of variants. Unlike Python's enum.Enum, Rust variants don't need values — the variant name itself is the data. The #[derive(Debug)] attribute auto-generates the {:?} formatter.
Enums with data
# Python: use classes or dataclasses for tagged unions class Shape: pass class Circle(Shape): def __init__(self, radius): self.radius = radius class Rectangle(Shape): def __init__(self, w, h): self.width, self.height = w, h shape = Circle(5.0)
#[derive(Debug)] enum Shape { Circle(f64), // tuple variant — holds a radius Rectangle(f64, f64), // holds width and height } fn main() { let shape = Shape::Circle(5.0); println!("{:?}", shape); }
Rust enums can carry data in each variant — each variant can have a different structure. This is a "tagged union" or "algebraic data type," far more powerful than Python's enum.Enum. Pattern matching extracts the inner data safely.
Option<T> (None safety)
# Python: None can appear anywhere def find_user(user_id): if user_id == 1: return "Alice" return None user = find_user(1) if user is not None: print(f"Found: {user}") else: print("Not found")
fn find_user(user_id: u32) -> Option<&'static str> { if user_id == 1 { Some("Alice") } else { None } } fn main() { match find_user(1) { Some(user) => println!("Found: {user}"), None => println!("Not found"), } }
Option is Rust's null-safety mechanism. A function returning Option forces callers to handle the None case explicitly — there is no way to accidentally use a None as if it were a value. Python's None can appear in any variable without any compiler enforcement.
if let (concise Option handling)
numbers = [1, 2, 3] first = numbers[0] if numbers else None if first is not None: print(f"First: {first}")
let numbers = vec![1, 2, 3]; if let Some(first) = numbers.first() { println!("First: {first}"); }
if let Some(value) = expression is shorthand for a match that handles only one variant. It is idiomatic for when you only care about the Some case. The equivalent for None is if let None = ..., though .is_none() is cleaner.
if-let chains
settings = {"timeout": 30} # The walrus operator binds and tests in one condition: if (value := settings.get("timeout")) and value > 10: print(f"long timeout: {value}")
use std::collections::HashMap; fn main() { let settings: HashMap<&str, i32> = [("timeout", 30)].into_iter().collect(); // Edition 2024: chain a let pattern and a bool test with && if let Some(&value) = settings.get("timeout") && value > 10 { println!("long timeout: {value}"); } }
Stabilised in the 2024 edition, let chains let a let pattern and ordinary boolean tests be joined with && in one if — close kin to Python's walrus operator in if (value := settings.get("timeout")) and value > 10. The Rust Some(..) pattern also distinguishes "key missing" from "value present", which Python's .get() blurs by returning None. Before 2024 this needed a nested if let { if value > 10 { ... } }; a binding from an earlier link is visible to later links.
Error Handling
Result<T, E> vs exceptions
def divide(x, y): if y == 0: raise ValueError("Cannot divide by zero") return x / y try: print(divide(10, 2)) print(divide(10, 0)) except ValueError as error: print(f"Error: {error}")
fn divide(x: f64, y: f64) -> Result<f64, String> { if y == 0.0 { Err(String::from("Cannot divide by zero")) } else { Ok(x / y) } } fn main() { match divide(10.0, 2.0) { Ok(result) => println!("{result}"), Err(error) => println!("Error: {error}"), } match divide(10.0, 0.0) { Ok(result) => println!("{result}"), Err(error) => println!("Error: {error}"), } }
Rust uses Result instead of exceptions. Errors are values — the caller is forced to handle them at the type level. Python's try/except is invisible in signatures, so callers may not know what exceptions a function raises without reading the documentation.
The ? operator (error propagation)
def parse_and_double(text): try: number = int(text) return number * 2 except ValueError as error: raise ValueError(f"parse failed: {error}") from error try: print(parse_and_double("21")) print(parse_and_double("abc")) except ValueError as error: print(f"Error: {error}")
fn parse_and_double(text: &str) -> Result<i32, String> { let number = text.parse::<i32>().map_err(|e| e.to_string())?; Ok(number * 2) } fn main() { match parse_and_double("21") { Ok(result) => println!("{result}"), Err(error) => println!("Error: {error}"), } match parse_and_double("abc") { Ok(result) => println!("{result}"), Err(error) => println!("Error: {error}"), } }
The ? operator unwraps Ok or returns early with the Err value, propagating errors up the call stack. It is the idiomatic replacement for Python's raise ... from ... chains, and keeps happy-path code flat without deeply nested error checks.
unwrap and expect
# Python: trust that an operation will succeed text = "42" number = int(text) # raises ValueError if it fails print(number)
let text = "42"; let number = text.parse::<i32>().unwrap(); // panics if Err println!("{number}"); let number2 = text.parse::<i32>().expect("text must be a valid integer"); println!("{number2}");
.unwrap() extracts the Ok value or panics on Err, analogous to Python code that doesn't catch exceptions. .expect("message") is the same but includes a custom panic message. Use these only in tests or when you are certain the value is Ok.
panic! (unrecoverable errors)
# Python: raise an unrecoverable assertion def check_positive(n): assert n > 0, f"Expected positive, got {n}" return n print(check_positive(5))
fn check_positive(n: i32) -> i32 { if n <= 0 { panic!("Expected positive, got {n}"); } n } fn main() { println!("{}", check_positive(5)); }
panic! terminates the current thread with a message, analogous to Python's assert or raising an unhandled exception. Panics are for programming errors (broken invariants), not expected failure conditions — those should use Result.
let-else early return
def parse_and_double(text): try: number = int(text) except ValueError: print("not a number") return print(number * 2) parse_and_double("42")
fn parse_and_double(text: &str) { let Ok(number) = text.parse::<i32>() else { println!("not a number"); return; }; println!("{}", number * 2); } fn main() { parse_and_double("42"); }
A let ... else binding handles the failure case up front, much like Python's try/except with an early return: it pattern-matches on the success value and binds it in the surrounding scope, and if the match fails it runs the else block, which must diverge (return, break, continue, or panic!). Unlike if let, whose binding is trapped inside its block, the value from let-else remains in scope, so the rest of the function reads straight down with no extra indentation.
Traits
Defining a trait
# Python: abstract base class (ABC) from abc import ABC, abstractmethod class Describable(ABC): @abstractmethod def describe(self) -> str: pass
trait Describable { fn describe(&self) -> String; }
A Rust trait is like a Python abstract base class — it defines a set of methods a type must implement. Rust traits are enforced at compile time with zero runtime overhead, whereas Python ABCs are checked at instantiation time.
Implementing a trait
from abc import ABC, abstractmethod class Animal(ABC): @abstractmethod def speak(self) -> str: pass class Dog(Animal): def speak(self) -> str: return "Woof!" class Cat(Animal): def speak(self) -> str: return "Meow!" dog = Dog() cat = Cat() print(dog.speak()) print(cat.speak())
trait Animal { fn speak(&self) -> &str; } struct Dog; struct Cat; impl Animal for Dog { fn speak(&self) -> &str { "Woof!" } } impl Animal for Cat { fn speak(&self) -> &str { "Meow!" } } fn main() { let dog = Dog; let cat = Cat; println!("{}", dog.speak()); println!("{}", cat.speak()); }
Rust's impl TraitName for StructName is the equivalent of Python class inheritance from an ABC. Multiple traits can be implemented for the same struct. Unlike Python, traits cannot carry instance state — only behavior.
Display / __str__
class Temperature: def __init__(self, celsius): self.celsius = celsius def __str__(self): return f"{self.celsius}°C" def __repr__(self): return f"Temperature({self.celsius})" temp = Temperature(22.5) print(temp) print(repr(temp))
use std::fmt; struct Temperature { celsius: f64, } impl fmt::Display for Temperature { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "{}°C", self.celsius) } } impl fmt::Debug for Temperature { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "Temperature({})", self.celsius) } } fn main() { let temp = Temperature { celsius: 22.5 }; println!("{temp}"); // calls Display println!("{temp:?}"); // calls Debug }
The Display trait implements {} formatting (like Python's __str__), and Debug implements {:?} formatting (like __repr__). For types where the default debug format is sufficient, use #[derive(Debug)] instead.
Trait bounds (generics)
# Python: duck typing — no bounds needed def print_all(items): for item in items: print(item) print_all([1, 2, 3]) print_all(["a", "b", "c"])
use std::fmt::Display; fn print_all<T: Display>(items: &[T]) { for item in items { println!("{item}"); } } fn main() { print_all(&[1, 2, 3]); print_all(&["a", "b", "c"]); }
Rust generics use trait bounds to express constraints on type parameters. T: Display means "any type that implements Display." Python uses duck typing — if the object has the required method, it works; Rust enforces the contract at compile time.
Cargo & Modules
Cargo.toml vs requirements.txt
# requirements.txt # requests==2.32.3 # flask==3.1.0 # pyproject.toml (modern) # [project] # dependencies = ["requests>=2.32", "flask>=3.1"]
# Cargo.toml # [package] # name = "my-project" # version = "0.1.0" # edition = "2021" # # [dependencies] # serde = { version = "1.0", features = ["derive"] } # reqwest = "0.12"
Cargo.toml is Rust's project manifest, combining the roles of Python's pyproject.toml and requirements.txt. Running cargo add reqwest adds a dependency; cargo build downloads and compiles everything. There is no separate "activate virtualenv" step.
use / import
import math from math import sqrt, pi from collections import defaultdict print(math.floor(3.7)) print(sqrt(16)) print(pi)
use std::f64::consts::PI; use std::collections::HashMap; fn main() { println!("{}", (3.7_f64).floor()); println!("{}", 16.0_f64.sqrt()); println!("{PI}"); let _map: HashMap<&str, i32> = HashMap::new(); }
Rust's use brings a path into scope, like Python's from module import name. The standard library is available via the std:: prefix without any imports — use just shortens the path.
Modules
# Python: each file is a module # math_utils.py: # def add(x, y): return x + y # def square(n): return n * n # main.py: # import math_utils # print(math_utils.add(3, 4))
// In Rust: modules can live inline or in separate files mod math_utils { pub fn add(x: i32, y: i32) -> i32 { x + y } pub fn square(n: i32) -> i32 { n * n } } fn main() { println!("{}", math_utils::add(3, 4)); println!("{}", math_utils::square(5)); }
Rust modules can be inline (using mod name { }) or in separate files (src/math_utils.rs). Items are private by default — use pub to expose them. Python modules are always public; everything in a .py file is accessible by default.
Cargo commands vs pip / python
# pip install package-name → install dependency # python main.py → run script # python -m pytest → run tests # pip freeze > requirements → lock deps
# cargo new project-name → create project # cargo add package-name → add dependency # cargo run → compile & run # cargo test → run tests # cargo build --release → optimized build # cargo check → type-check without building
Cargo is Rust's all-in-one build tool, package manager, and test runner. Unlike Python's fragmented toolchain (pip, venv, pytest, setuptools), Cargo handles everything. cargo check is especially useful during development — it verifies types without the cost of code generation.