Variables in Rust behave differently from most other programming languages. By default, every variable you create is immutable — you cannot change it after binding a value to it. This is not a limitation; it is a deliberate design choice that helps the compiler catch bugs and enables optimizations.
Declaring Variables with `let`
In Rust, variables are declared using the let keyword:
let x = 5;
Attempting to reassign an immutable variable is a compile-time error:
let x = 5;
x = 10; // error: cannot assign twice to immutable variable
To make a variable mutable, add the mut keyword:
let mut x = 5;
x = 10; // OK
This design principle — immutability by default — forces you to think carefully about which data in your program actually needs to change. In many codebases, the majority of variables should be immutable.
Type Annotations
Rust is a statically typed language, meaning every variable has a known type at compile time. Rust can often infer the type automatically, but you can also annotate it explicitly:
let a: u8 = 123; // explicit type annotation
let b = 123456789; // compiler infers i32
When the type cannot be inferred, Rust requires an explicit annotation. It is good practice to annotate types when writing library code or when the inferred type may be surprising.
Integer Types
Rust provides a rich set of integer types covering both signed and unsigned values across multiple sizes:
| Type | Width | Range |
|---|---|---|
i8 | 8 bits | -128 to 127 |
i16 | 16 bits | -32,768 to 32,767 |
i32 | 32 bits | -2,147,483,648 to 2,147,483,647 |
i64 | 64 bits | Very large signed range |
i128 | 128 bits | Enormous signed range |
u8 | 8 bits | 0 to 255 |
u16 | 16 bits | 0 to 65,535 |
u32 | 32 bits | 0 to 4,294,967,295 |
u64 | 64 bits | Very large unsigned range |
u128 | 128 bits | Enormous unsigned range |
let a: u8 = 123; // 8-bit unsigned — range 0..255
println!("a = {}", a);
let mut b: i8 = 0;
b = 42;
println!("b = {}", b);
The default integer type when not specified is i32, which is generally the fastest integer type on most architectures:
let c = 123456789; // inferred as i32
Checking the Size of a Variable
You can use std::mem::size_of_val to inspect the memory footprint of any variable at runtime:
use std::mem;
let mut a = 123456789;
println!("a = {}, size = {} bytes", a, mem::size_of_val(&a));
// a = 123456789, size = 4 bytes (i32)
Architecture-Sized Integers: `isize` and `usize`
Rust provides two special integer types that adapt to the word size of the target architecture:
isize: Signed, pointer-sized integerusize: Unsigned, pointer-sized integer
On a 32-bit system, these are 32 bits. On a 64-bit system, they are 64 bits. The most important use of usize is as an array index, since arrays in Rust are indexed by usize:
use std::mem;
let z: isize = 123;
let size_of_z = mem::size_of_val(&z);
println!("z = {}, takes up {} bytes, {}-bit OS", z, size_of_z, size_of_z * 8);
let idx: usize = 0;
let arr = [10, 20, 30];
println!("arr[0] = {}", arr[idx]);
// arr[0] = 10
Floating-Point Types
Rust provides two floating-point types following the IEEE 754 standard:
| Type | Width | Precision |
|---|---|---|
f32 | 32 bits | Single precision |
f64 | 64 bits | Double precision |
The default floating-point type is f64 because on modern CPUs it is roughly the same speed as f32 while offering twice the precision:
let x: f32 = 3.14; // single precision
let y = 2.5; // inferred as f64
The Boolean Type
Rust's boolean type is bool with exactly two values: true and false. It occupies one byte in memory:
let is_active = true;
let is_done: bool = false;
let g = false;
println!("{}", g); // false
let positive = 5 > 0;
println!("{}", positive); // true
Booleans are the foundation of all conditional logic in Rust.
The Character Type
The char type in Rust represents a single Unicode Scalar Value. Unlike C where char is one byte, Rust's char is four bytes (32 bits) to accommodate the full Unicode character set:
let letter: char = 'a';
let emoji: char = '';
let chinese: char = '';
char in C: 1 byte (ASCII only, 128 characters)
char in Rust: 4 bytes (Unicode, 1,114,112 characters)
Type Inference
Rust's compiler performs type inference, meaning you often do not need to write the type explicitly. The compiler deduces the type from the context:
let x = 5; // i32
let y = 5.0; // f64
let z = true; // bool
let c = 'R'; // char
However, type inference has limits. When the compiler cannot determine the type unambiguously, it will ask you to annotate it:
let v = Vec::new(); // error: type annotations needed
let v: Vec<i32> = Vec::new(); // OK
Integer Literals and Readability
Rust allows underscores in numeric literals to improve readability. This does not affect the value:
let million = 1_000_000; // same as 1000000
let hex = 0xFF_AA_BB; // hexadecimal
let binary = 0b1111_0000; // binary
let octal = 0o77; // octal
let byte = b'A'; // byte literal (u8)
You can also suffix literals directly with their type:
let x = 42u8; // u8
let y = 3.14f32; // f32
Integer Overflow
In debug mode, integer overflow causes a panic (runtime crash) in Rust. In release mode (--release), it wraps around silently. This means overflow bugs are caught during development and testing.
If you intentionally want wrapping arithmetic, Rust provides explicit methods:
let x: u8 = 255;
let y = x.wrapping_add(1); // y = 0 (wraps around)
let z = x.saturating_add(1); // z = 255 (saturates at max)
let w = x.checked_add(1); // w = None (overflow detected)
Summary of Primitive Types
Primitive Types in Rust:
+------------------+--------+----------------------------+
| Type | Size | Values |
+------------------+--------+----------------------------+
| i8 / u8 | 1 byte | signed / unsigned 8-bit |
| i16 / u16 | 2 bytes| signed / unsigned 16-bit |
| i32 / u32 | 4 bytes| signed / unsigned 32-bit |
| i64 / u64 | 8 bytes| signed / unsigned 64-bit |
| i128 / u128 | 16 bytes| signed / unsigned 128-bit |
| isize / usize | arch | pointer-sized integer |
| f32 | 4 bytes| single-precision float |
| f64 | 8 bytes| double-precision float |
| bool | 1 byte | true / false |
| char | 4 bytes| Unicode scalar value |
+------------------+--------+----------------------------+
A Complete Example
use std::mem;
fn main() {
// Immutable integer
let a: u8 = 123;
println!("a = {}, size = {} bytes", a, mem::size_of_val(&a));
// Mutable integer
let mut b: i8 = 0;
b = 42;
println!("b = {}", b);
// Architecture-sized integer
let z: isize = 123;
let size_of_z = mem::size_of_val(&z);
println!("z = {}, takes up {} bytes, {}-bit OS", z, size_of_z, size_of_z * 8);
// Array indexing with usize
let idx: usize = 0;
let arr = [1, 2, 3];
println!("arr[{}] = {}", idx, arr[idx]);
// Character
let d: char = 'a';
println!("d = {}, size = {} bytes", d, mem::size_of_val(&d));
// Float
let e = 2.5f64;
println!("e = {}", e);
// Boolean
let g = false;
println!("g = {}", g);
let h = 5 > 0;
println!("h = {}", h);
}
Conclusion
Rust's type system is one of its greatest strengths. Immutability by default eliminates accidental mutation bugs. The rich set of integer types lets you express intent precisely — use u8 for a byte, i32 for a general integer, usize for an array index. The compiler's type inference means you rarely need to write types explicitly, but when you do, the annotations become living documentation.
Understanding these fundamentals is essential for everything that follows in Rust, from ownership and borrowing to generic programming and concurrent code.