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 →
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.
