Rust variables look simple at first: give a value a name, then use that name later. But Rust adds an important rule immediately: bindings are immutable by default. That single choice shapes how Rust code communicates intent. A value that should stay stable stays stable unless you explicitly say otherwise.
This lesson follows OSRust.001, where you installed the Rust toolchain, used Cargo, compiled a first program, and saw an early ownership example. Here the focus is narrower: let, mut, constants, shadowing, scope, and just enough type inference to understand what the compiler is doing. The deeper Rust type system belongs to the next lesson.
1. What A Rust Variable Really Is
In beginner Rust, you normally introduce a named value with let. Programmers often call this a variable declaration, but Rust documentation frequently describes it as a binding: the name is bound to a value.
fn main() {
let miners = 12;
println!("Miners online: {miners}");
}
The compiler sees the integer literal 12, infers a suitable integer type, binds the name miners to that value, and then allows the name to be used later in the same scope. You do not have to write the type every time because Rust performs type inference when the intended type is clear.
If terminal commands such as cargo run are still unfamiliar, revisit the Cargo workflow in OSRust.001. If a command works in one terminal but not another, the earlier PATH environment variable explainer is also useful background.
2. Variables Are Immutable By Default
Rust deliberately makes ordinary let bindings immutable. Once a value is bound, you cannot assign a different value to that same binding unless you explicitly opt into mutability.
fn main() {
let power_mw = 10;
println!("Site power: {power_mw} MW");
power_mw = 12;
println!("Site power: {power_mw} MW");
}
This does not compile. The Rust compiler reports that you cannot assign twice to the immutable variable. The error is useful: the code told Rust that power_mw should not change, then contradicted that promise.
Immutability by default is not the same thing as saying Rust programs can never change state. It means a programmer must make changing state visible. That makes a later reader less likely to wonder whether any ordinary binding might be modified somewhere else in the function.
3. Use mut When The Same Binding Must Change
Add mut after let when the same binding is expected to receive new values. The keyword is short, but its meaning is important: this value is intentionally allowed to change.
fn main() {
let mut temperature_c = 24;
println!("Start: {temperature_c} C");
temperature_c = 27;
println!("Later: {temperature_c} C");
}
Here both assignments belong to the same mutable binding. This is appropriate when a value represents changing state: a retry counter, an accumulated total, a position updated in a loop, or a measurement that is intentionally replaced during a procedure.
Do not add mut automatically. If the value never needs reassignment, leave it immutable. Rust will even warn when a binding is marked mutable but never actually changes.
4. Constants Are Different From Immutable Variables
A Rust const is always immutable, requires an explicit type annotation, and is intended for values that can be determined as a constant expression. Constants are declared with const, not let, and the normal naming convention is uppercase letters with underscores.
const SECONDS_PER_HOUR: u32 = 60 * 60;
const MAX_RETRIES: u8 = 5;
fn main() {
println!("One hour is {SECONDS_PER_HOUR} seconds");
println!("Maximum retries: {MAX_RETRIES}");
}
An immutable let binding and a constant can both resist reassignment, but they communicate different ideas. Use a normal binding for a value created as part of program execution. Use a constant for a fixed value that conceptually belongs to the program or module and meets Rust’s constant-expression rules.
5. Shadowing Creates A New Binding
Rust allows a later let statement to reuse a name. This is called shadowing. Shadowing is not mutation. Instead of changing the original binding, Rust creates a new binding that uses the same name.
fn main() {
let hashrate = 100;
let hashrate = hashrate + 25;
let hashrate = hashrate * 2;
println!("Hashrate index: {hashrate}");
}
The final value is 250, but each let hashrate = ... creates a new binding. This matters because the newer binding can even have a different type.
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("Number of spaces: {spaces}");
}
The first spaces is text. The second spaces is a number. Shadowing makes this legal because the second line introduces a new binding. A mutable binding cannot simply change from one unrelated type to another while remaining the same binding.
6. mut Versus Shadowing
Use mut when you truly have one piece of state whose value changes over time. Use shadowing when you have reached a new stage of processing and no longer need to refer to the old form by its old name.
- Use
mut: a counter increases repeatedly inside a loop. - Use
mut: a buffer is filled or edited in several steps. - Use shadowing: raw input is parsed into a more useful representation.
- Use shadowing: a value is normalized or transformed once and the old form is no longer needed.
- Use shadowing: the transformed value should keep the same meaningful name even if its type changes.
The difference is semantic, not merely stylistic. Mutation says, “this binding changes.” Shadowing says, “this is a new binding, and from this point forward this name refers to the new value.”

7. Scope Controls Where A Binding Exists
A variable is only available where its scope allows it to exist. Curly braces create blocks, and an inner block can introduce its own binding with the same name as one outside the block.
fn main() {
let mode = "normal";
{
let mode = "diagnostic";
println!("Inside block: {mode}");
}
println!("Outside block: {mode}");
}
Inside the braces, mode refers to the inner binding. After the block ends, that inner binding is no longer in scope, so mode again refers to the outer value. This is a basic form of lexical scope, and you will see the same idea repeatedly when Rust functions, loops, ownership, and borrowing become more complex.
8. Type Inference Keeps Simple Bindings Concise
Rust is statically typed, meaning the compiler needs to know the type of every value before the program runs. That does not mean you must manually write every type. The compiler often infers it from the value and how the value is used.
fn main() {
let rack_count = 8;
let efficiency = 14.5;
let online = true;
let site_name = "North Campus";
println!("{rack_count} {efficiency} {online} {site_name}");
}
Rust can infer useful types for all four bindings. When inference would be ambiguous, or when you want to document the expected type, add an annotation.
fn main() {
let rack_count: u32 = 8;
let efficiency: f64 = 14.5;
let online: bool = true;
println!("{rack_count} {efficiency} {online}");
}
For now, read the annotation syntax as name: type. The next Rust lesson will slow down and study Rust’s core data types directly rather than trying to teach the entire type system here.
9. Multiple Bindings And Destructuring
let can bind more than one name at once when the value has a matching structure. A simple tuple example is useful because it shows that bindings are connected to patterns, not merely single names.
fn main() {
let (voltage, current) = (240, 30);
println!("Voltage: {voltage} V");
println!("Current: {current} A");
}
The tuple contains two values, and the pattern (voltage, current) creates two bindings. Pattern matching becomes much more powerful later in Rust, but this basic destructuring form is already practical.
10. Build A Variables Lab
Create a fresh Cargo project so every compiler message comes from a small, controlled program.
cargo new variables_lab
cd variables_lab
cargo run
Replace src/main.rs with this program:
const TARGET_TEMP_C: i32 = 25;
fn main() {
let site = "Lab A";
let mut temperature_c = 22;
println!("{site}: {temperature_c} C");
temperature_c = 24;
println!("Updated: {temperature_c} C");
let temperature_c = temperature_c + 1;
println!("Adjusted: {temperature_c} C");
{
let site = "Sensor Cabinet";
println!("Inner scope: {site}");
}
println!("Target: {TARGET_TEMP_C} C");
}
Run it with cargo run. Then make one deliberate mistake: remove mut but keep the reassignment. Read the compiler error before restoring the keyword. Compiler-guided practice is one of the fastest ways to learn what Rust’s rules actually mean.
11. Common Beginner Mistakes
- Trying to reassign an immutable binding: add
mutonly if reassignment is truly intended. - Thinking shadowing is mutation: another
letcreates a new binding. - Trying to mark a constant with
mut: constants cannot be mutable. - Forgetting a constant’s type:
constdeclarations require an explicit type. - Expecting an inner binding to exist after its block ends: scope controls name visibility and lifetime.
- Over-annotating obvious values: let inference work when the type is clear; annotate when clarity or ambiguity requires it.
12. Exercises
- Create an immutable binding named
facilitycontaining a short site name and print it. - Create
let mut fan_speed = 40;, change it to55, and print both stages. - Remove
mutfrom the previous exercise and record the compiler error. - Create a constant named
SECONDS_PER_MINUTEwith typeu32. - Shadow a text binding with its character or byte length and print the new value.
- Create an outer variable named
status, shadow it inside a block, then print it again after the block to prove the outer binding still exists. - Create a tuple containing a voltage and current value and destructure it into two bindings.
- Write one example where
mutis clearer than shadowing and one where shadowing is clearer thanmut.
13. Knowledge Check
- What keyword normally creates a Rust binding?
- Are ordinary Rust bindings mutable or immutable by default?
- What keyword allows reassignment of the same binding?
- Can a Rust
constbe mutable? - Why does a constant declaration include an explicit type?
- Does shadowing mutate the old binding?
- Can shadowing create a new binding with a different type?
- What happens to a binding declared inside a block after the block ends?
- What does type inference mean?
- When is
mutusually clearer than shadowing?
14. Answers
let.- Immutable by default.
mut.- No.
- Rust requires the type to be written for constants, which also makes the constant’s intended representation explicit.
- No. A new
letbinding is created with the same name. - Yes.
- It goes out of scope and can no longer be accessed through that block-local binding.
- The compiler determines a value’s type from available context without requiring you to write the type manually.
- When one logical piece of state is intentionally updated repeatedly over time.
15. Official Reference And Prior Lessons
- The Rust Programming Language — Variables and Mutability
- OSRust.001 — What Is Rust? rustup, rustc, Cargo, Ownership, and Your First Program
- What Is Git and GitHub? Version Control, Commits, and Branches
- What Is Syntax Highlighting? Why Code Editors Use Different Colors
- IT: What Is the PATH Environment Variable?
16. What You Should Remember
Rust starts variables from a conservative position. let creates an immutable binding unless you explicitly add mut. Constants use const and require a type. Shadowing creates a new binding rather than modifying the old one. Blocks create scope boundaries. Type inference keeps ordinary code concise while still allowing the compiler to know every type before execution.
Next in the canonical Rust sequence is OSRust.003: Types, where integer sizes, floating-point types, booleans, characters, tuples, arrays, and explicit annotations can be studied without overloading this variables lesson.
BitcoinVersus.Tech
Advertisement
BitcoinVersus.Tech publishes open technical education across programming, semiconductors, electronics, networking, robotics, data centers, operating systems, firmware, and infrastructure.
Editor’s Note
Compiler behavior can evolve across Rust releases. When a local compiler message differs from a lesson example, read the complete diagnostic and confirm current syntax against the official Rust documentation.
We volunteer daily to help keep the information on this platform verifiably accurate. Support our independent research through the support options available on BitcoinVersus.Tech.
BitcoinVersus.tech is not a financial advisor. Content is provided for informational purposes.

Leave a Reply