Rust Variables, Data Types, and Constants: The Complete Guide

Rust Variables, Data Types, and Constants: The Complete Guide
In many modern languages like JavaScript or Python, declaring variables is fluid. The runtime figures out the type on the fly, and you can freely reassign an integer variable into a string later.
Rust rejects this. Rust is a statically typed language, which means the compiler must know the data type of all variables at compile time. However, unlike Java or C, Rust employs a highly sophisticated Type Inference engine. Let's delve into how Rust handles memory binding, the concept of forced immutability, and the primitive types that make up the foundation of the language.
1. Variables and Immutability
By default, when you bind a value to a variable in Rust using the let keyword, that variable is immutable. You cannot change the value after the initial assignment.
This is a core design choice meant to encourage safety. When multiple threads access data, immutability ensures that the data won't unexpectedly change state beneath them.
fn main() {
let spaces = 5;
// spaces = 10; // compiler error! Cannot assign twice to immutable variable
println!("We need {} spaces.", spaces);
}If you uncomment the reassignment line, cargo run will immediately throw an error: E0384: cannot assign twice to immutable variable.
Opting In To Mutability
To make a variable mutable in Rust, you must explicitly opt-in using the mut keyword. This forces you to be intentional about memory mutation, serving as a clear flag to anyone reading your code that the state of this data is fluid.
fn main() {
let mut energy_level = 10;
println!("Energy is at: {}", energy_level);
energy_level = 15; // Completely valid!
println!("Energy increased to: {}", energy_level);
}2. Shadowing
Rust provides a powerful feature called Shadowing. You can declare a new variable with the exact same name as a previous variable. The new variable "shadows" the previous one, essentially rendering the old variable inaccessible.
fn main() {
let x = 5;
// x is shadowed by this new physical allocation
let x = x + 1;
{
// Shadowing is scope-specific!
let x = x * 2;
println!("Inner scope physical x: {}", x); // Prints 12
}
println!("Outer scope physical x: {}", x); // Prints 6
}Shadowing vs mut
Why use shadowing instead of simply using mut?
Shadowing fundamentally creates a brand new allocation. This allows you to perform transformations on the data and even change the underlying data type completely, which mut does not allow.
// Shadowing (Allowed)
let spaces = " "; // Type: &str
let spaces = spaces.len(); // Type: usize
// Mutability (Rejected!)
let mut spaces_string = " ";
// spaces_string = spaces_string.len(); // Error: expected `&str`, found `usize`3. The Physical Data Types
Rust categorizes its types into two broad subsets: Scalar (representing a single value) and Compound (grouping multiple values).
Scalar Types
Integers
Rust provides specific bit-width integers to accommodate exact memory alignments.
- Signed:
i8,i16,i32,i64,i128, andisize(pointer size, depends on 64-bit or 32-bit architecture). - Unsigned:
u8,u16,u32,u64,u128, andusize.
[!TIP] What should you use? Unless you are optimizing for a highly constrained embedded system, default to
i32. Rust's compiler heavily optimizesi32logic on modern standard architectures. When indexing into arrays or vectors, the language forces you to useusize.
Floating-Point Numbers
Rust supports f32 (single precision) and f64 (double precision). On modern CPUs, f64 executes at roughly the same speed as f32 but offers drastically more precision, making it the default.
Booleans and Characters
bool: Contains exactly one byte and can only resolve totrueorfalse.char: Unlike C, where acharis one byte, Rust encapsulates Unicode. A Rustcharis exactly four bytes in size. It can represent ASCII, Chinese, Japanese, Korean ideographs, and emojis directly.
Compound Types
Compound types map directly to contiguous memory.
Tuples (())
Tuples allow you to group a variety of types into one compound structure. Tuples have a fixed length; once declared, they cannot grow or shrink.
fn main() {
// Explicit type annotation not generally required, but useful for clarity
let network_status: (i32, f64, u8) = (500, 6.4, 1);
// Destructuring a tuple
let (status_code, latency, retry_count) = network_status;
// Direct index access via dot notation
let pure_status = network_status.0;
}Arrays ([])
Unlike a Tuple, every element of an Array must possess the exact same type. In Rust, an Array is allocated entirely on the Stack, not the Heap, making it incredibly fast. Because it lives on the Stack, an Array must have a known, fixed length at compile time.
fn main() {
let months = ["January", "February", "March", "April"];
// Type annotation syntax: [Type; Length]
let error_codes: [i32; 3] = [400, 404, 500];
// Array initialization shorthand: Fill 5 elements with the value 0
let memory_buffer = [0; 5]; // [0, 0, 0, 0, 0]
// Accessing values
let server_error = error_codes[2]; // 500
}If you attempt to access an index out of bounds (e.g., error_codes[5]), the Rust compiler will often catch it. If the index is determined dynamically at runtime, Rust will panic safely rather than enabling a buffer-overflow vulnerability like C/C++.
4. Constants vs Static Variables
There is a stricter tier of immutability than a let binding: the const.
Constants are values bound to a name that cannot ever be changed. They must be explicitly typed, and they can only be set to constant expressions—never to a runtime computation.
// Constants reside in global scope and are universally evaluated.
const MAX_CONCURRENT_TRANSACTIONS: u32 = 100_000;A common naming convention in Rust is to use screaming snake case (ALL_CAPS) for constants, and to use underscores for readability in large numeric literals.
Unlike variables which represent distinct memory allocation requests on the stack, constants act closer to #define macros from C. Internally, the Rust compiler effectively "inline pastes" the value of the constant anywhere the identifier is used in code.
Conclusion
Understanding memory allocation requires understanding data types and how the compiler locks them in place. The type system in Rust is undeniably strict. The distinction between an i32 and usize is rigid, and the compiler will force you to cast between them explicitly to avoid unintended memory layout conversions.
By pushing immutability by default, and enabling explicit memory tracking via types, Rust begins the process of eliminating unintended state changes from your application.
Now that we understand how to allocate data, we must dissect how to instruct the program to execute logic across it. In the next section, we examine Rust's unique approach to expression-heavy control flow and its terse function mechanics.
Read next: Rust Functions and Control Flow →
Frequently Asked Questions
Q: Why are Rust variables immutable by default?
Immutability by default forces you to be intentional about state changes, which eliminates whole classes of bugs where a value is modified unexpectedly. When you declare a variable with let, the compiler rejects any attempt to change it unless you explicitly add mut. This design means that when you see let mut in code, it is an immediate signal that this value changes — making code easier to reason about. It also enables compiler optimisations: an immutable binding can safely be cached, inlined, or placed in read-only memory.
Q: What is the difference between shadowing and mutability in Rust?
Shadowing (let x = x + 1) creates a new variable with the same name, potentially of a different type, while the original is dropped. Mutability (let mut x = 5; x = 6) modifies the same variable in place, and the type cannot change. Shadowing is used to transform a value through a series of steps using the same name without needing mut, and is particularly common when converting a string to a number (let input: String = ...; let input: u32 = input.trim().parse().unwrap()). They are distinct mechanisms that serve different purposes.
Q: What is the difference between i32 and u32, and how do I choose?
i32 is a signed 32-bit integer (range −2,147,483,648 to 2,147,483,647). u32 is unsigned (range 0 to 4,294,967,295). Use signed integers when the value can legitimately be negative (offsets, temperatures, differences). Use unsigned when the value is inherently non-negative (counts, sizes, indices). Rust's default integer type is i32 because it is the fastest integer type on most platforms. For sizes and lengths, use usize — it is pointer-sized and is what collection indexing and the standard library use for lengths.
Quick Knowledge Check
Why might a developer use Shadowing over a simple mut declared variable?
- Shadowing bypasses compile-time type checking, allowing unsafe memory access.
- Shadowing allows for changes to the underlying data type within the same scope string. ✓
- Shadowing automatically moves the allocation from the Stack to the Heap for faster access.
- Shadowing is deprecated in Rust 2026, so it should never be favored over mut.
Explanation: Because shadowing creates a completely new variable behind the scenes, you can repurpose an identifier name while changing the physical data type (e.g. shadowing a String length into an integer), which 'mut' strictly forbids.
