Every variable in Rust has a scope: the region of code where it is valid. When a variable goes out of scope, it is automatically dropped and its memory is freed. This deterministic cleanup is fundamental to Rust's memory safety guarantees and is the foundation of the RAII pattern.
Variable Scope
A variable's scope is defined by the curly braces {} that surround it. Any variable declared inside a block is only accessible within that block and its inner blocks:
fn scope_example() {
let a = 123;
{
let b = 2;
println!("b = {}", b); // OK: b is in scope
println!("a = {}", a); // OK: a is in outer scope, still visible
}
println!("a = {}", a); // OK: a is still in scope here
// println!("b = {}", b); // ERROR: b is out of scope
}
Scope Visualization:
+------------------------------------------+
| fn scope_example() { |
| let a = 123; <-- lives here |
| +--------------------------------+ |
| | { | |
| | let b = 2; <-- lives here | |
| | // both a and b accessible | |
| | } <-- b is dropped here | |
| +--------------------------------+ |
| // only a is accessible here | |
| } <-- a is dropped here | |
+------------------------------------------+
This is not just a lexical rule — when a variable goes out of scope, Rust automatically calls the drop function on it, freeing any resources it holds (heap memory, file handles, network connections, etc.).
Shadowing
Rust allows you to declare a new variable with the same name as an existing variable in the same or inner scope. This is called shadowing. The new declaration shadows the old one:
fn scope_and_shadowing() {
let a = 123;
let a = 777; // shadows the previous `a` — now a = 777
{
let a = 555; // shadows the outer `a` — now a = 555 inside this block
println!("a inside block = {}", a); // a = 555
}
// Back to the outer `a`
println!("a outside block = {}", a); // a = 777
}
Key observations:
- The inner
a = 555only shadows within the inner block. When the block ends, the shadow ends and the outera = 777becomes visible again. - Shadowing is not mutation. Each
let a = ...creates a new binding. The previous binding still exists for the duration of its own scope.
Shadowing vs. Mutation
Shadowing and mutation look similar but are fundamentally different:
// Mutation: requires mut, same type
let mut x = 5;
x = 6; // OK, same type (i32 -> i32)
// Shadowing: can change type entirely
let y = "hello"; // &str
let y = y.len(); // usize — completely different type!
println!("y = {}", y); // y = 5
Shadowing allows you to reuse a name while changing the type. This is especially useful when you want to transform data through several steps without creating multiple differently-named variables:
// Without shadowing: forced to invent names
let spaces_str = " ";
let spaces_len = spaces_str.len();
// With shadowing: same name, transformed value
let spaces = " ";
let spaces = spaces.len(); // now spaces is a usize
Why Shadowing Is Valuable
Shadowing is a design pattern in Rust that improves code clarity:
- Type transformations: Convert a string to a number, parse a value, or trim whitespace without carrying the original binding around.
- Intermediate calculations: Compute through several steps using the same logical name.
- Scope-limited overrides: Override a value within a block without affecting the outer scope.
fn process_input(input: &str) -> usize {
let input = input.trim(); // shadow: &str -> &str (trimmed)
let input = input.parse::<usize>() // shadow: &str -> Result<usize, _>
.expect("Failed to parse");
input // returns the usize
}
Global Variables with `const`
Rust provides two mechanisms for global variables. The first is const, which declares a compile-time constant:
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265358979;
fn main() {
println!("Max points: {}", MAX_POINTS);
println!("Pi: {}", PI);
}
Key properties of const:
- Must have an explicit type annotation — type inference is not available for constants.
- No memory address — the compiler replaces every use of the constant with its value directly (like a
#definein C). - Evaluated at compile time — the value must be a compile-time-computable expression.
- Always immutable —
const mutdoes not exist. - Global scope: can be declared at any scope, including outside functions.
Naming convention: constants use SCREAMING_SNAKE_CASE.
Global Variables with `static`
The second mechanism is static, which declares a variable with a fixed memory address that lives for the entire duration of the program:
static GREETING: &str = "Hello, Rust!";
static mut COUNTER: u8 = 0;
fn main() {
println!("{}", GREETING);
// Accessing or modifying a mutable static requires unsafe
unsafe {
COUNTER = 15;
println!("Counter: {}", COUNTER);
}
}
Key properties of static:
- Has a memory address — the same address throughout program execution.
- Immutable by default —
static mutexists but requiresunsafeto access. - Lives for the entire program — the
'staticlifetime.
`const` vs `static` Comparison
| Property | const | static |
|---|---|---|
| Memory address | No (inlined by compiler) | Yes (fixed address) |
| Mutability | Never | static mut (unsafe) |
| Evaluated at | Compile time | Compile time |
| Lifetime | N/A (inlined) | 'static (entire program) |
| Use case | Named constants, magic numbers | Global state, FFI data |
| Type annotation | Required | Required |
Why Mutable Statics Require `unsafe`
Mutable global state is one of the most common sources of bugs in concurrent programs. If multiple threads can read and write a static mut variable simultaneously without synchronization, the result is a data race — undefined behavior.
Rust's safety model requires you to acknowledge this risk explicitly:
static mut B_GLOBAL: u8 = 10;
fn main() {
unsafe {
B_GLOBAL = 15;
println!("{}", B_GLOBAL);
}
}
By wrapping the access in unsafe, you tell the compiler and your colleagues: "I know this is risky. I am taking responsibility for ensuring this access is safe."
For safe global mutable state, prefer standard library synchronization types like Mutex or RwLock:
use std::sync::Mutex;
static COUNTER: Mutex<u32> = Mutex::new(0);
fn increment() {
let mut count = COUNTER.lock().unwrap();
*count += 1;
}
Complete Scope and Shadowing Example
const APP_VERSION: &str = "1.0.0";
static APP_NAME: &str = "MyApp";
fn demonstrate_scope() {
let x = 10;
println!("x = {}", x); // x = 10
let x = x * 2; // shadow: x is now 20
println!("x after shadow = {}", x); // x = 20
{
let x = x + 5; // shadow within inner block: x = 25
println!("x in block = {}", x); // x = 25
}
println!("x after block = {}", x); // x = 20 (outer shadow still active)
}
fn main() {
println!("{} v{}", APP_NAME, APP_VERSION);
demonstrate_scope();
}
Conclusion
Rust's scoping rules enforce a clean lifecycle for every variable: born at declaration, dropped at the closing brace. This predictability is the foundation of Rust's automatic memory management.
Shadowing is more than a convenience feature — it is a principled way to transform data across types while keeping names meaningful and code readable. Combined with const for compile-time constants and static for program-lifetime data, Rust gives you precise control over where data lives and how long it survives.
Understanding scope and shadowing deeply will prepare you for Rust's ownership and borrowing system, where these concepts take on even greater significance.