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
x = 5
x = 10 # rebind freely
x = "hello" # different type, no problemlet x = 5;
// x = 10; // error: cannot assign twice
let mut y = 5;
y = 10; // OK with mutType annotation
age: int = 30 # hint only, not enforced
age = "thirty" # still works at runtimelet age: i32 = 30;
// age = "thirty"; // compile error: mismatched typesType 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
x = 42 # int (dynamic)
y = 3.14 # float (dynamic)
name = "Ada" # str (dynamic)let x = 42; // i32 (inferred)
let y = 3.14; // f64 (inferred)
let name = "Ada"; // &str (inferred)Function signatures
def add(a: int, b: int) -> int:
return a + b # hints optionalfn 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
x = "5"
x = int(x) # rebind to different type
print(x + 1) # => 6let x = "5";
let x: i32 = x.parse().unwrap();
println!("{}", x + 1); // => 6Shadowing in a transformation
data = " 42 "
data = data.strip()
data = int(data)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
MAX_RETRIES = 5 # convention only
PI = 3.14159
# MAX_RETRIES = 10 # oops, nothing stops youconst MAX_RETRIES: u32 = 5; // enforced
const PI: f64 = 3.14159;
// MAX_RETRIES = 10; // compile error