Variables and Types
Prerequisites: Your First Project
Next: Functions and Control Flow
This article covers Rust's foundational building blocks: how to declare variables, control mutability, understand the fixed-width numeric types, and work with the two string types.
Hello, World
# Python
print("Hello, world!")
// Rust
fn main() {
println!("Hello, world!");
}
What changed:
println!is a macro (note the!), not a function. Macros are expanded at compile time and can accept variable numbers of arguments with format strings.- Every Rust executable needs a
fn main()entry point. - Statements end with
;.
Formatting:
name = "ranker"
score = 0.92
print(f"Service {name} scored {score:.2f}")
let name = "ranker";
let score = 0.92_f64;
println!("Service {name} scored {score:.2}");
Python's f-strings and Rust's format strings are nearly identical in syntax.
Variables and Mutability
Python — everything is mutable by default
x = 10
x = 20 # rebind, no problem
x += 5 # mutate, no problem
MY_CONST = 42 # convention only; Python won't stop you from reassigning it
Rust — immutable by default, explicit opt-in to mutation
let x = 10;
// x = 20; // compile error: cannot assign twice to immutable variable
let mut x = 10; // `mut` opts in to mutation
x = 20; // now fine
x += 5;
const MAX_RETRIES: u32 = 3; // true compile-time constant; type required
Why immutable by default? If you accidentally mutate something you didn't mean to, you get a compile error instead of a subtle runtime bug. In a data pipeline you often want to pass a dataset around and be sure nothing upstream is modifying it.
Shadowing — re-use the same name with a different value or type
# Python: same variable, type can silently change
value = "42"
value = int(value) # now an int
// Rust: shadowing creates a new variable that hides the old one
let value = "42";
let value = value.parse::<i64>().unwrap(); // new `value`, now an i64
println!("{value}"); // 42
Shadowing is useful for transformation chains: you keep the same logical name without needing mut, and each step can have a different type.
Primitive Types
Python — types are inferred; ints are arbitrary precision
x: int = 42
y: float = 3.14
z: bool = True
Rust — types are fixed-width; you choose the size
// Integers
let a: i8 = -128; // signed 8-bit [-128, 127]
let b: i32 = -2_000_000; // signed 32-bit (Python's default int range)
let c: i64 = 9_000_000_000; // signed 64-bit
let d: u8 = 255; // unsigned 8-bit [0, 255]
let e: u64 = 18_000_000_000_000_000_000;
let f: usize = 42; // pointer-sized; used for indices and lengths
// Floats
let g: f32 = 3.14_f32; // 32-bit float
let h: f64 = 3.141_592_653; // 64-bit float (Python's default)
// Booleans
let i: bool = true; // lowercase, not True
// Characters (Unicode scalar, 4 bytes)
let j: char = 'λ';
Underscores in numeric literals are readability separators — 1_000_000 is 1000000. Same idea as Python.
Type inference — Rust infers types from usage, so you rarely write annotations in practice:
let batch_size = 32; // inferred i32
let learning_rate = 0.001; // inferred f64
let enabled = false; // inferred bool
Arithmetic and Casting
# Python: implicit widening, no overflow for int
result = 255 + 1 # → 256 (no overflow)
ratio = 7 / 2 # → 3.5 (true division)
quot = 7 // 2 # → 3 (floor division)
// Rust: explicit casting with `as`; integer overflow panics in debug mode
let result: u8 = 255_u8.wrapping_add(1); // → 0 (explicit wrap)
let ratio = 7.0_f64 / 2.0; // → 3.5
let quot = 7 / 2; // → 3 (integer division, like //)
// Cast between types explicitly — no implicit conversion
let n: i32 = 42;
let f: f64 = n as f64; // like Python's float(n)
Strings
Python has one string type. Rust has two that serve different purposes.
| Python | Rust String |
Rust &str |
|
|---|---|---|---|
| Ownership | GC-managed | heap-allocated, owned | borrowed slice |
| Mutability | immutable (but re-bindable) | mutable if mut |
always read-only |
| Analogy | str |
list that holds chars |
memoryview or slice |
# Python — one type, zero friction
greeting = "hello"
greeting += " world" # creates a new string, rebinds
print(len(greeting))
// &str — a borrowed view into string data (usually a string literal)
let greeting: &str = "hello";
// String — an owned, growable heap string
let mut owned = String::from("hello");
owned.push_str(" world"); // append in-place
println!("{}", owned.len()); // 11
Common String Operations
s = " inference-server "
print(s.strip()) # "inference-server"
print(s.upper()) # " INFERENCE-SERVER "
print("server" in s) # True
print(s.replace("server", "worker"))
parts = "a,b,c".split(",") # ["a", "b", "c"]
joined = ",".join(["a", "b", "c"]) # "a,b,c"
print(f"batch={32}")
let s = " inference-server ";
println!("{}", s.trim()); // "inference-server"
println!("{}", s.to_uppercase()); // " INFERENCE-SERVER "
println!("{}", s.contains("server")); // true
println!("{}", s.replace("server", "worker"));
let parts: Vec<&str> = "a,b,c".split(',').collect();
let joined = ["a", "b", "c"].join(","); // "a,b,c"
println!("batch={}", 32);
Converting Between String and &str
let owned: String = String::from("hello");
let borrowed: &str = &owned; // String → &str (cheap borrow)
let back: String = borrowed.to_string(); // &str → String (allocates)
Think of &str as a read-only window into a String or a string literal. Pass &str to functions when you don't need ownership; return String when you're building a new value.
See Also
- Ownership — why
Stringand&strexist as separate types - Functions and Control Flow — next article in this chapter
- Python → Rust Cheatsheet — quick reference table