Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Compare
  3. Python vs Rust
  4. Python vs Rust Functions

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

Python
def add(a: int, b: int) -> int:
    return a + b

print(add(2, 3))  # => 5
Rust
fn add(a: i32, b: i32) -> i32 {
    a + b  // implicit return (no semicolon)
}
println!("{}", add(2, 3)); // => 5

No return value

Python
def greet(name: str) -> None:
    print(f"Hello, {name}")
Rust
fn greet(name: &str) {
    println!("Hello, {name}");
    // returns () implicitly
}
Warm-up1 / 2

Can you write this from memory?

Write the function header for `add_item` with one parameter named `item`

Python: Python Function Arguments Practice: defaults, *args, keyword-only, **kwargs →

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

Python
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"
Rust
fn first<T: Clone>(items: &[T]) -> T {
    items[0].clone()
}

println!("{}", first(&[10, 20]));   // => 10
println!("{}", first(&["a", "b"])); // => "a"

Trait bounds

Python
# Python: duck typing, no bounds needed
def show(item):
    print(item)  # works if __str__ exists
Rust
// 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

Python
def make_adder(n):
    def adder(x):
        return x + n
    return adder

add5 = make_adder(5)
print(add5(3))  # => 8
Rust
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

let add5 = make_adder(5);
println!("{}", add5(3)); // => 8

Closure as argument

Python
nums = [3, 1, 4, 1, 5]
nums.sort(key=lambda x: -x)
# => [5, 4, 3, 1, 1]
Rust
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

Python
def connect(host: str, port: int = 5432):
    print(f"{host}:{port}")

connect("localhost")       # => localhost:5432
connect("localhost", 3306) # => localhost:3306
Rust
fn 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:3306

Builder pattern for many options

Python
def fetch(url, *, timeout=30, retries=3):
    ...

fetch("https://api", retries=5)
Rust
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() };

Practice Both Languages

10 free exercises a day. No credit card required. Build syntax muscle memory with spaced repetition.

Free forever. No credit card required.

← Back to Python vs Rust
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.