Python vs Rust Functions
Function signatures, type requirements, generics, and closures in Python versus Rust.
Function Signatures
Python's def keyword creates a function. Type hints on parameters and return values are optional: def add(a, b): return a + b is valid even without annotations. Rust's fn keyword requires type annotations on every parameter and the return type (unless the function returns nothing, i.e., the unit type ()). fn add(a: i32, b: i32) -> i32 { a + b } cannot compile without those types. This strictness is the price of static analysis -- the compiler uses these signatures to check every call site at compile time. Python developers often find mandatory signatures annoying for the first week and then appreciate them: they serve as documentation that never goes stale. Another surface difference is return syntax. Python uses an explicit return statement. Rust functions return the value of the last expression when it lacks a semicolon: a + b (no semicolon) is an implicit return, while a + b; (with semicolon) evaluates to () and the function returns nothing. Forgetting a semicolon is a compile error if the return type expects a value, and accidentally adding one is a compile error if the return type is non-unit. The semicolon-as-suppressor pattern feels counterintuitive at first, but it makes Rust functions behave like expressions, enabling chains like if/else blocks that resolve to values.
Function definition
def add(a: int, b: int) -> int:
return a + b
print(add(2, 3)) # => 5fn add(a: i32, b: i32) -> i32 {
a + b // implicit return (no semicolon)
}
println!("{}", add(2, 3)); // => 5No return value
def greet(name: str) -> None:
print(f"Hello, {name}")fn greet(name: &str) {
println!("Hello, {name}");
// returns () implicitly
}Can you write this from memory?
Write the function header for `add_item` with one parameter named `item`
Generics
Python functions are generic by default because the language is dynamically typed -- def first(items): return items[0] accepts any sequence type without annotation. Adding type hints introduces generic syntax via TypeVar: T = TypeVar("T"); def first(items: list[T]) -> T: .... Rust generics use angle-bracket syntax: fn first<T>(items: &[T]) -> &T. The critical difference is that Rust generics are monomorphized at compile time: the compiler generates a separate machine-code version of first for each concrete type used. Python generic annotations are erased at runtime and never affect execution. Rust generics also require trait bounds when you use the generic type. Writing fn print_item<T>(item: T) fails if the function body calls println! on item, because not all types implement Display. You must write fn print_item<T: std::fmt::Display>(item: T). Python has no such requirement -- duck typing means any object with the right method signatures works. The trait-bound requirement feels restrictive coming from Python, but it guarantees that generic functions cannot be called with incompatible types. Python discovers those mismatches at runtime; Rust catches them during compilation.
Generic function
from typing import TypeVar, Sequence
T = TypeVar("T")
def first(items: Sequence[T]) -> T:
return items[0]
print(first([10, 20])) # => 10
print(first(["a", "b"])) # => "a"fn first<T: Clone>(items: &[T]) -> T {
items[0].clone()
}
println!("{}", first(&[10, 20])); // => 10
println!("{}", first(&["a", "b"])); // => "a"Trait bounds
# Python: duck typing, no bounds needed
def show(item):
print(item) # works if __str__ exists// Rust: must declare Display bound
fn show<T: std::fmt::Display>(item: T) {
println!("{item}");
}Closures
Python closures are functions defined inside other functions that capture variables from the enclosing scope. The nonlocal keyword is required to modify a captured variable; without it, assignment creates a new local. Rust closures use a pipe-delimited parameter syntax: |x, y| x + y. Under the hood, the Rust compiler determines whether a closure captures variables by reference, by mutable reference, or by value (move), and assigns the closure one of three traits: Fn, FnMut, or FnOnce. Python has no such distinction; all captured variables are effectively references to the enclosing scope. The ownership-aware capture system matters when closures are passed to threads or stored in data structures. A closure that moves ownership of captured variables (using the move keyword) can be sent to another thread because no shared mutable state exists. Python's equivalent -- passing a closure to a thread -- requires locking or message passing to avoid data races, with no compiler enforcement. For everyday use, Rust closures behave like Python lambdas with the restriction that multi-statement closures use curly braces: |x| { let doubled = x * 2; doubled + 1 }. Unlike Python's single-expression lambdas, Rust closures can contain arbitrary statements and blocks.
Basic closure
def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
print(add5(3)) # => 8fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
let add5 = make_adder(5);
println!("{}", add5(3)); // => 8Closure as argument
nums = [3, 1, 4, 1, 5]
nums.sort(key=lambda x: -x)
# => [5, 4, 3, 1, 1]let mut nums = vec![3, 1, 4, 1, 5];
nums.sort_by_key(|&x| std::cmp::Reverse(x));
// => [5, 4, 3, 1, 1]Default and Optional Arguments
Python supports default parameter values directly in the function signature: def connect(host, port=5432). Rust has no default arguments. The standard workaround is a builder pattern, a struct with default fields, or a separate constructor function. For simple cases, Rust functions accept Option<T> parameters: fn connect(host: &str, port: Option<u16>) and call port.unwrap_or(5432) inside the body. The builder pattern is more common for functions with many optional parameters: ConnectionBuilder::new("localhost").port(5432).timeout(30).build(). Python developers find this verbose, and it is -- Rust intentionally avoids hidden defaults that make call sites ambiguous. Every argument is explicit at the call site, which aids readability at the cost of typing. Python's **kwargs for unlimited named arguments has no Rust equivalent. Rust's type system requires every parameter to be known at compile time, so open-ended argument sets are modeled with structs, enums, or trait objects. The adjustment is significant for Python developers who rely on **kwargs for configuration, but the Rust alternatives produce more self-documenting code because each field has a name and a type.
Default / optional parameters
def connect(host: str, port: int = 5432):
print(f"{host}:{port}")
connect("localhost") # => localhost:5432
connect("localhost", 3306) # => localhost:3306fn connect(host: &str, port: Option<u16>) {
let p = port.unwrap_or(5432);
println!("{host}:{p}");
}
connect("localhost", None); // => localhost:5432
connect("localhost", Some(3306)); // => localhost:3306Builder pattern for many options
def fetch(url, *, timeout=30, retries=3):
...
fetch("https://api", retries=5)struct FetchConfig {
timeout: u32,
retries: u32,
}
impl Default for FetchConfig {
fn default() -> Self {
Self { timeout: 30, retries: 3 }
}
}
let cfg = FetchConfig { retries: 5, ..Default::default() };