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

Python vs Rust Variables

How variable binding, mutability, and type inference differ between Python and Rust.

Binding and Mutability

Python variables are names attached to objects. Assigning x = 5 binds the name x to an integer object, and x = "hello" rebinds it to a string -- no type constraint, no permission required. Rust takes the opposite stance: let x = 5 creates an immutable binding by default. Attempting x = 10 on the next line produces a compiler error. You opt into mutability explicitly with let mut x = 5. This distinction is not merely stylistic. Immutable-by-default means the Rust compiler can reason about data flow more aggressively, enabling optimizations and catching unintended mutations at compile time. Python relies on convention and linting to discourage unexpected rebinding; Rust enforces it. The practical adjustment for Python developers is small -- you add mut when you know a variable will change -- but the mindset shift runs deeper. Rust asks you to decide upfront whether a value will change, which forces you to think about data flow before writing the logic. Python encourages experimentation first and refactoring later. Neither philosophy is wrong, but they produce different coding rhythms.

Immutable vs mutable binding

Python
x = 5
x = 10  # rebind freely
x = "hello"  # different type, no problem
Rust
let x = 5;
// x = 10; // error: cannot assign twice
let mut y = 5;
y = 10; // OK with mut

Type annotation

Python
age: int = 30  # hint only, not enforced
age = "thirty"  # still works at runtime
Rust
let age: i32 = 30;
// age = "thirty"; // compile error: mismatched types

Type Inference

Python is dynamically typed: the interpreter never checks types at compile time (there is no compile time). Type hints exist for tooling (mypy, Pyright) but do not affect runtime behavior. Rust is statically typed with strong local inference. Writing let x = 42 assigns x the type i32 without an explicit annotation, because the compiler infers the type from the literal. The inference engine is local to each function body -- it does not infer parameter types or return types from call sites (unlike Haskell's global inference). This means function signatures always carry explicit types: fn add(a: i32, b: i32) -> i32. Python type hints look superficially similar (def add(a: int, b: int) -> int:), but they are optional decorations that can be wrong without consequence. Rust types are checked by the compiler and rejected on mismatch. For Python developers, the adjustment is writing types on every function boundary. Inside function bodies, inference handles most bindings without annotation. The payoff is that type errors surface during compilation rather than hiding until a specific code path runs in production.

Inferred types

Python
x = 42        # int (dynamic)
y = 3.14      # float (dynamic)
name = "Ada"  # str (dynamic)
Rust
let x = 42;        // i32 (inferred)
let y = 3.14;      // f64 (inferred)
let name = "Ada";  // &str (inferred)

Function signatures

Python
def add(a: int, b: int) -> int:
    return a + b  # hints optional
Rust
fn add(a: i32, b: i32) -> i32 {
    a + b  // types required
}

Shadowing

Rust allows variable shadowing within the same scope: you can write let x = 5; followed by let x = x + 1; and the second binding replaces the first. This is not reassignment -- it creates a new binding that happens to reuse the name. The old value is dropped (or moved). Shadowing also permits type changes: let x = "5"; let x: i32 = x.parse().unwrap(); first binds x as a string, then shadows it with an integer. Python has no equivalent mechanism. Rebinding x = 5 then x = "hello" looks similar but operates differently: Python just re-labels the name, while Rust creates a distinct stack slot. The practical benefit of Rust shadowing is that you can transform a value through a pipeline of names without inventing new identifiers (raw_input, parsed_input, validated_input). Each let x shadows the previous x, keeping the scope clean. The trade-off is readability: heavy shadowing makes it harder to track which x you are looking at. The Rust community considers moderate shadowing idiomatic, especially for parse-and-transform sequences, but advises against shadowing across dozens of lines.

Shadowing with type change

Python
x = "5"
x = int(x)  # rebind to different type
print(x + 1)  # => 6
Rust
let x = "5";
let x: i32 = x.parse().unwrap();
println!("{}", x + 1); // => 6

Shadowing in a transformation

Python
data = "  42  "
data = data.strip()
data = int(data)
Rust
let data = "  42  ";
let data = data.trim();
let data: i32 = data.parse().unwrap();

Constants

Python uses ALL_CAPS naming to signal constants (MAX_SIZE = 100), but nothing prevents reassignment. Rust has two compile-enforced mechanisms: const and static. A const value is inlined at every usage site and must have a type annotation: const MAX_SIZE: u32 = 100. A static value has a fixed memory address and lives for the entire program. static items can be mutable (static mut), but accessing mutable statics is unsafe because of potential data races. Python developers accustomed to treating uppercase names as "do not touch" constants will appreciate Rust's compiler enforcement -- attempting to reassign a const is a hard error. The deeper difference is that Rust constants must be computable at compile time (const-evaluable expressions only). Python has no such restriction since there is no compilation phase. This means Rust constants cannot call arbitrary functions or allocate heap memory, while Python "constants" are just variables assigned at module load time and can hold anything. For truly computed constants in Rust, the once_cell or std::sync::OnceLock pattern fills the gap.

Constants

Python
MAX_RETRIES = 5  # convention only
PI = 3.14159
# MAX_RETRIES = 10  # oops, nothing stops you
Rust
const MAX_RETRIES: u32 = 5;  // enforced
const PI: f64 = 3.14159;
// MAX_RETRIES = 10; // compile error

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.