3.1. Variables and Mutability
This chapter contains interview questions and answers for 3.1. Variables and Mutability chapter.
Variable Bindings and Names
Q1
Question:
What does let actually introduce in Rust? Explain the distinction between a binding, a name, and the value associated with that binding.
Short answer:
let introduces a binding: an association between a name and a value. The name is an identifier used to refer to that binding. If the name is shadowed later, a new binding can take over that name.
Interview answer:
When you write let x = 5;, Rust introduces a binding named x and initializes it with the value 5. The identifier x is the name used by later expressions to resolve that binding. Shadowing is important because the same name can subsequently be associated with a different binding, for example through another let x = .... So it is useful to distinguish the identifier x, the binding it currently resolves to, and the value associated with that binding.
Deep dive:
The distinction explains why mut and shadowing behave differently. Assignment through mut changes the value of an existing binding, whereas a repeated let introduces a new binding and causes later uses of the name to resolve to that new binding.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let x = 6;
}
The second statement introduces a new binding named x; it does not reassign the first binding.
Common pitfall:
Treating the name itself as the value, or describing shadowing as if the original binding had simply been mutated.
Possible follow-up:
- What happens if
let x = ...is used again with the same name? - How does
mutdiffer from shadowing?
Q2
Question:
Why are let bindings immutable by default in Rust? What guarantees does that default give the compiler and the programmer?
Short answer:
A binding declared without mut cannot be reassigned. That makes the absence of mut a compile-time guarantee that the particular binding will not be assigned a new value.
Interview answer:
Rust makes bindings immutable by default so that changing a value requires an explicit mut. The important guarantee is compile-time: if a binding is not mutable, an assignment to that binding is rejected by the compiler. This lets readers rely on the fact that the particular binding will not be reassigned, rather than having to discover that through control-flow reasoning or convention. The chapter presents this as a way to make code easier to reason about and to prevent bugs caused by conflicting assumptions about whether a value changes. (Rust Documentation)
Deep dive:
The guarantee is specifically about reassignment through that binding. It should not be paraphrased as a universal claim about whether some underlying value can ever change through any other access path; that is a broader Rust topic not covered by this chapter.
Example:
#![allow(unused)]
fn main() {
let x = 5;
x = 6; // error[E0384]
}
Common pitfall:
Saying “immutable means this value can never change by any mechanism.” In the context of this chapter, the precise statement is that this binding cannot be reassigned.
Possible follow-up:
- Why is the error detected at compile time?
- What changes when
mutis added?
Q3
Question:
What does mut change about a binding? Explain both what it permits the compiler to accept and what it communicates to readers of the code.
Short answer:
In the context of this chapter, mut permits later assignment to the existing binding. It also signals to readers that the binding’s value is intended to change.
Interview answer:
Adding mut makes the binding assignable after initialization. For example, let mut x = 5; x = 6; is valid, whereas the same assignment through an immutable binding is rejected. mut is also an explicit design signal: it tells readers that this binding is expected to change. The chapter presents both the compiler-enforced rule and the readability benefit as reasons the keyword is useful. (Rust Documentation)
Deep dive:
mut changes mutability, not the binding’s type. Reassignment to the same binding must remain type-correct. Reusing the same name with a different type requires shadowing instead.
Example:
#![allow(unused)]
fn main() {
let mut x = 5;
x = 6;
}
Common pitfall:
Thinking mut means the binding can change from one type to another. It does not.
Possible follow-up:
- What happens if a
mutbinding is assigned a value of a different type? - How does that differ from shadowing?
Q4
Question:
Distinguish initialization from assignment in Rust. Why does this distinction matter when reasoning about let, mut, and shadowing?
Short answer:
let x = ... introduces and initializes a binding. x = ... assigns a new value to an existing binding and therefore requires that binding to be mutable.
Interview answer:
Initialization creates a binding and gives it its initial value, as in let x = 5;. Assignment without let, as in x = 6;, targets an existing binding. If that binding is not mutable, the compiler rejects the assignment. Shadowing is a third case: let x = 6; after an earlier x is another initialization that creates a new binding rather than assigning to the old one.
Deep dive:
This distinction explains most of the chapter’s important contrasts. mut affects assignment to an existing binding, while shadowing happens through another let, which introduces a new binding.
Example:
#![allow(unused)]
fn main() {
let x = 5; // initialize a binding
let x = 6; // initialize a new shadowing binding
}
Common pitfall:
Reading let x = 6; as “reassign x to 6.” The let means a new binding is being introduced.
Possible follow-up:
- Does
let x = 6;require the earlierxto bemut? - What happens if you use
x = 7;after that?
Q5
Question:
Consider:
#![allow(unused)]
fn main() {
let x = 5;
x = 6;
}
Why does this fail to compile? What exactly is Rust preventing?
Short answer:
The second line is an assignment to the existing binding x, but that binding was declared without mut. Rust therefore rejects the assignment at compile time.
Interview answer:
let x = 5; creates an immutable binding. The next line, x = 6;, is not another declaration because there is no let; it attempts to assign a new value to that existing binding. Because the binding is not mutable, the compiler rejects the assignment with E0384, “cannot assign twice to immutable variable.” (Rust Documentation)
Deep dive:
There are two different fixes depending on intent. If the existing binding should change, use let mut x = 5;. If the intention is to introduce another value under the same name, use shadowing: let x = 6;.
Example:
#![allow(unused)]
fn main() {
let mut x = 5;
x = 6;
}
Common pitfall:
Adding mut automatically without first deciding whether the code actually needs mutation. Shadowing may better express the intended transformation.
Possible follow-up:
- What compiler error is emitted?
- How would you fix it using shadowing instead of
mut?
Shadowing
Q8
Question:
What is variable shadowing in Rust, and how is it fundamentally different from reassigning a mutable binding?
Short answer:
Shadowing introduces a new binding with let using an existing name. Reassignment writes a new value to the existing binding through mut.
Interview answer:
When Rust sees another let x = ..., it creates a new binding named x, and subsequent uses of x resolve to that new binding. By contrast, x = ... without let assigns to the existing binding, provided that binding is mutable. Because shadowing creates a new binding, the new binding can have a different type and its mutability is determined independently by its own let declaration. (Rust Documentation)
Deep dive:
The key distinction is binding identity. Shadowing changes which binding the name resolves to; assignment changes the value associated with the existing binding.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1; // new binding
let mut y = 5;
y = y + 1; // assignment to the existing binding
}
Common pitfall:
Calling shadowing “mutation.” Shadowing and mutation are different mechanisms.
Possible follow-up:
- Can shadowing change the type?
- Is the new shadowed binding mutable by default?
Q9
Question:
What happens here?
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;
}
Explain what each let statement does and whether the original binding has been mutated.
Short answer:
The first let creates x = 5. The second evaluates x + 1 using the existing x, then creates a new binding x = 6. The original binding is not mutated.
Interview answer:
The first statement creates the initial binding with value 5. For the second statement, the initializer x + 1 is evaluated using the binding that already exists. That produces 6, and the let then introduces a new binding named x containing 6. From that point onward, the second binding is the one selected when x is used.
Deep dive:
This is shadowing rather than reassignment because the second statement uses let. The new binding can therefore have properties different from the old binding, including a different type.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;
}
Common pitfall:
Saying that x was mutated from 5 to 6. No existing binding was reassigned.
Possible follow-up:
- Which
xdoes the right-hand side use? - Would this work if the first binding were immutable? Why?
Q11
Question:
What does this program print? Which binding does each use of x refer to?
#![allow(unused)]
fn main() {
let x = 5;
{
let x = x + 1;
println!("{x}");
}
println!("{x}");
}
Short answer:
It prints 6 and then 5.
Interview answer:
The outer x is 5. Inside the block, the inner let x = x + 1 uses the outer x to compute 6 and then creates a new inner binding named x. The inner println! therefore sees 6. When the block ends, that inner binding is no longer in scope, so the outer x becomes the binding selected by the name again, and the final println! prints 5. (Rust Documentation)
Deep dive:
The inner declaration does not change the outer binding. It shadows it only within the nested scope.
Example:
6
5
Common pitfall:
Assuming the inner let permanently changes the outer x.
Possible follow-up:
- What would happen if the inner
letwere replaced byx = x + 1? - Why does the outer
xbecome visible again after the block?
Q14
Question:
Can shadowing occur without introducing a new lexical scope? Explain with an example and distinguish shadowing in the same scope from shadowing in a nested scope.
Short answer:
Yes. Repeating let in the same scope is enough to shadow a previous binding. A nested block adds a separate lexical scope, which limits the new binding’s visibility.
Interview answer:
Shadowing does not require a new { ... } block. For example:
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;
}
The second let shadows the first in the same scope. A nested block can also introduce a shadowing binding, but then the new binding is limited to that inner scope. Once the block ends, the outer binding becomes visible again.
Deep dive:
The mechanism is the repeated let; lexical scope determines how long the shadowing binding remains visible. These are related but distinct concepts.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1; // same-scope shadowing
{
let x = 10; // nested-scope shadowing
println!("{x}");
}
println!("{x}");
}
Common pitfall:
Assuming that shadowing itself creates a new scope. It does not.
Possible follow-up:
- What happens if the name is shadowed three times in the same scope?
- What determines which binding a use of the name resolves to?
Q16
Question:
Explain why this compiles:
#![allow(unused)]
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
but this does not:
#![allow(unused)]
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
What single underlying distinction explains both outcomes?
Short answer:
The first case creates a new binding, so the new spaces can have type usize. The second assigns to the existing &str binding, so the assigned usize does not match its type.
Interview answer:
The root distinction is whether the operation creates a new binding or assigns to an existing one. In the first example, the second let shadows the first binding. spaces.len() returns a usize, and the new binding can therefore have type usize. In the second example, mut permits assignment to the existing binding, but it does not change that binding’s type. The original binding is &str, while spaces.len() is usize, so the assignment fails with a type mismatch. (Rust Documentation)
Deep dive:
This is a particularly useful causal explanation in interviews: mut versus shadowing is fundamentally a distinction between modifying one binding and introducing another. The type behavior follows from that distinction.
Example:
#![allow(unused)]
fn main() {
let mut spaces = " ";
// spaces = spaces.len(); // type mismatch: expected `&str`, found `usize`
}
Common pitfall:
Memorizing only “mut cannot change types” without understanding that shadowing works because a different binding is being created.
Possible follow-up:
- Is the second
spacesbinding mutable? - What compiler error does the
mutversion produce?
Q20
Question:
Does the following compile? Explain exactly which binding the final assignment refers to and whether that binding is mutable.
#![allow(unused)]
fn main() {
let x = 5;
let x = 6;
x = 10;
}
Short answer:
No. The final assignment targets the second x, which was created by let x = 6; without mut, so that binding is immutable.
Interview answer:
There are two bindings named x. The second let shadows the first, so at the final line the name x resolves to the second binding. Because that binding was declared without mut, the bare assignment x = 10; is rejected with E0384.
Deep dive:
Shadowing does not grant mutability. Every new let establishes the mutability of its own binding. If the second declaration were let mut x = 6;, the final assignment would be allowed.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let mut x = 6;
x = 10;
}
Common pitfall:
Assuming that once a name has been shadowed it becomes assignable. Mutability belongs to the particular binding.
Possible follow-up:
- What single change would make the example compile?
- Does the first
xaffect the final assignment?
Q21
Question:
What is printed here? Trace the active binding through each scope.
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("{x}");
}
println!("{x}");
}
Short answer:
It prints 12 and then 6.
Interview answer:
The first binding is x = 5. The next let shadows it with a new binding initialized from 5 + 1, so the active outer-scope binding becomes x = 6. Inside the block, another let shadows that binding with x = 12. The inner println! therefore prints 12. When the block ends, the inner binding is no longer in scope, so the previous outer binding, x = 6, is selected again, and the final println! prints 6. This mirrors the chapter’s example. (Rust Documentation)
Deep dive:
The important reasoning pattern is to track the active binding at each point rather than thinking in terms of one variable whose value is repeatedly changed.
Example:
12
6
Common pitfall:
Assuming the 12 survives after the block ends.
Possible follow-up:
- How many distinct bindings named
xare introduced? - Which binding does the inner initializer use?
Q22
Question:
What happens here? Identify every distinct binding and explain which x is mutated by x += 1.
#![allow(unused)]
fn main() {
let mut x = 5;
{
let x = 10;
println!("{x}");
}
x += 1;
println!("{x}");
}
Short answer:
It prints 10 and then 6. The inner x shadows the outer one only inside the block; after the block ends, x += 1 applies to the outer mut binding.
Interview answer:
The outer declaration creates a mutable x with value 5. The inner let x = 10 creates a separate, immutable binding that shadows the outer one inside the block, so the first println! prints 10. After the block ends, the inner binding is no longer in scope and the outer mutable x becomes the selected binding again. x += 1 therefore changes the outer binding from 5 to 6.
Deep dive:
This combines shadowing and mutation. The let inside the block changes which binding the name resolves to; += later mutates an existing binding.
Example:
10
6
Common pitfall:
Assuming x += 1 modifies the inner x simply because that was the most recently declared x.
Possible follow-up:
- Would
x += 1compile if it were inside the inner block? - What are the mutability properties of the two bindings?
Q24
Question:
Does this compile, and why?
#![allow(unused)]
fn main() {
let x = 5;
let mut x = 10;
x = 15;
}
How does this differ from a case where the second let is not mut?
Short answer:
Yes. The second let creates a new mutable binding that shadows the first, so x = 15 assigns to that mutable binding.
Interview answer:
The first declaration creates an immutable x. The second declaration introduces another binding with the same name, but this time it is declared mut, so the new binding is mutable. The assignment therefore targets the second binding and is valid. If the second declaration were just let x = 10;, the assignment would fail because the currently visible binding would be immutable.
Deep dive:
Mutability does not carry over from or depend on the previous binding. Each binding’s mutability is established by its own declaration.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let mut x = 10;
x = 15;
}
Common pitfall:
Assuming a shadowed binding inherits the mutability of the binding it shadows.
Possible follow-up:
- Which binding does
x = 15modify? - Can an immutable binding shadow a mutable one?
Q25
Question:
What happens here? Identify each binding and explain which binding is mutable.
#![allow(unused)]
fn main() {
let x = 5;
{
let mut x = x + 1;
x += 1;
println!("{x}");
}
println!("{x}");
}
Short answer:
It prints 7 and then 5. The inner let mut creates a new mutable binding initialized from the outer x; only that inner binding is mutated.
Interview answer:
The outer binding is x = 5 and is immutable. Inside the block, let mut x = x + 1 first reads the outer x, computes 6, and creates a new mutable binding that shadows the outer one. x += 1 then assigns through that inner mutable binding, changing it to 7. When the block ends, the inner binding is out of scope and the original outer x is still 5.
Deep dive:
This example separates two operations that are often conflated: let mut x = ... performs initialization of a new shadowing binding, while x += 1 performs mutation of that binding.
Example:
7
5
Common pitfall:
Thinking the inner x += 1 modifies the outer x.
Possible follow-up:
- What would the final value be if
x += 1were removed? - Is the outer binding ever mutable?
Q26
Question:
Why is this valid Rust even though the type associated with the name x changes?
#![allow(unused)]
fn main() {
let x = "hello";
let x = x.len();
let x = x * 2;
}
What would be different if these operations were performed through a single mut binding?
Short answer:
Each let creates a new binding, so the successive bindings can have different types. A single mut binding keeps its original type, so assigning a usize where the binding was initialized as &str would fail.
Interview answer:
The three declarations introduce three bindings. The first has a string-slice type, the second has the usize returned by len, and the third remains a usize. Because each declaration creates a new binding, the fact that the same name is reused does not require one binding to change type. With a single mut binding, however, every assignment targets the same binding, whose type was established by its initialization, so changing from &str to usize would be a type error. (Rust Documentation)
Deep dive:
Rust remains statically typed throughout this example. The type does not dynamically change; instead, several statically typed bindings happen to use the same name successively.
Example:
#![allow(unused)]
fn main() {
let mut x = "hello";
// x = x.len(); // error: expected `&str`, found `usize`
}
Common pitfall:
Describing this as dynamic typing. The name is reused; the individual bindings remain statically typed.
Possible follow-up:
- How many distinct bindings are introduced?
- Could the final
xbe made mutable independently of the earlier bindings?
Q28
Question:
Consider:
#![allow(unused)]
fn main() {
let x = 5;
{
let x = 10;
{
let x = 20;
println!("{x}");
}
println!("{x}");
}
println!("{x}");
}
What is printed, and how does name resolution change as each nested scope begins and ends?
Short answer:
It prints 20, then 10, then 5. Each inner binding shadows the outer binding within its scope, and when the inner scope ends, the enclosing binding becomes the one selected again.
Interview answer:
There are three bindings named x, one per scope. The innermost block selects x = 20, so the first print is 20. After that block ends, its binding is no longer in scope, so the middle block’s x = 10 is selected. After the middle block ends, the outer x = 5 is selected again. The output is therefore 20, 10, 5.
Deep dive:
This is the same lexical-shadowing rule as the simpler nested example, just repeated over three scopes. The important skill is tracing which binding the name resolves to at each point.
Example:
20
10
5
Common pitfall:
Thinking an inner shadow permanently changes the outer binding.
Possible follow-up:
- How many distinct bindings named
xare introduced? - What happens to each inner binding when its scope ends?
Q29
Question:
Compare mut and shadowing along three dimensions: whether a new binding is created, whether the type can change, and whether the resulting binding is mutable.
Short answer:
mut permits assignment to the same binding, whose type stays the same. Shadowing creates a new binding, which can have a different type; that new binding is immutable unless its own declaration uses mut.
Interview answer:
The first distinction is binding identity: assignment through mut operates on the existing binding, while shadowing with let creates a new binding. Because mut does not create a new binding, its type remains the same for assignments to that binding. Shadowing can change type because each let creates a separately typed binding. Finally, mutability is determined separately for each declaration: mut makes that binding mutable, while a shadowed binding is immutable unless the new declaration is itself let mut. (Rust Documentation)
Deep dive:
The type difference and mutability difference both follow from the binding distinction. Shadowing does not “relax” type checking; it starts a new binding with its own type and mutability.
Example:
#![allow(unused)]
fn main() {
let x = "hello";
let x = x.len();
let mut y = 5;
y = 6;
}
Common pitfall:
Reducing the comparison to only “shadowing changes type, mut does not.” The new binding’s mutability is also independently determined.
Possible follow-up:
- Can a shadowing
letcreate a mutable binding? - When is
mutmore appropriate than shadowing?
Q30
Question:
Suppose you repeatedly transform a value and want each transformed result to become the new meaning of the same name, with the final binding immutable. Why might shadowing be preferable to mut?
Short answer:
Shadowing lets each transformation create a new binding, and the final binding can remain immutable. It also permits type changes between stages.
Interview answer:
Shadowing is useful when the transformations are conceptually stages of a value rather than ongoing mutable state. Each let creates a new binding, so an intermediate stage can have a different type, and once the final let is reached the result is immutable unless mut is explicitly requested. A single mut binding would keep one binding throughout the process and would not allow its type to change.
Deep dive:
This is one reason the chapter recommends shadowing as an alternative to introducing names such as spaces_str and spaces_num. The same logical name can describe the value at successive stages of a transformation. (Rust Documentation)
Example:
#![allow(unused)]
fn main() {
let input = " ";
let input = input.len();
let input = input * 2;
}
The final input is immutable and has the final type.
Common pitfall:
Using mut merely because a value is computed in multiple steps, even when the final result is not intended to remain mutable.
Possible follow-up:
- What does shadowing provide that
mutcannot? - When would
mutbe a better representation?
Q32
Question:
What practical advantage does shadowing provide when a value passes through several transformations? Why might this be preferable to using names such as input_str, input_len, and ...?
Short answer:
It lets successive stages reuse one meaningful name while still allowing their types to differ. That can keep transformation-oriented code clearer and avoid artificial name proliferation.
Interview answer:
Without shadowing, a transformation pipeline can require different names merely because each stage has a different type. Shadowing lets the program reuse a name such as input for each successive representation. This can make the code express the fact that the value is conceptually being transformed from one stage to another, without sacrificing static typing.
Deep dive:
The chapter specifically illustrates this with spaces changing from a string to a number. Shadowing avoids having to choose names like spaces_str and spaces_num. (Rust Documentation)
Example:
#![allow(unused)]
fn main() {
let input = " ";
let input = input.len();
}
Common pitfall:
Treating shadowing as a performance feature. Its benefit here is primarily naming and code clarity.
Possible follow-up:
- Is there a runtime cost to using shadowing instead of distinct names?
- When might distinct names be clearer?
Q33
Question:
Could you describe shadowing as rebinding rather than mutation? Explain why that terminology is more precise.
Short answer:
Yes. Shadowing introduces a new binding under an existing name; mutation changes the value associated with an existing mutable binding.
Interview answer:
“Rebinding” or “introducing a new binding” is more precise because shadowing does not assign a new value to the old binding. A repeated let creates another binding and causes subsequent uses of the name to resolve to it. Mutation, by contrast, means assigning a new value to an existing mutable binding. This distinction explains why shadowing can change type while assignment through mut cannot. (Rust Documentation)
Deep dive:
The terminology is useful because it predicts behavior. If you think an operation is mutation, you expect the existing binding’s type and identity to remain the same. If it is shadowing, you should expect a new binding with independently determined properties.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let x = 6; // shadowing: new binding
let mut y = 5;
y = 6; // mutation through assignment
}
Common pitfall:
Using “mutate” and “shadow” interchangeably.
Possible follow-up:
- Why can shadowing change the type?
- Does shadowing modify the old binding?
Q34
Question:
An interviewer says: “Rust variables are immutable unless you use mut, so shadowing is just another form of mutation.” How would you correct that statement?
Short answer:
The first part is correct, but shadowing is not mutation. Shadowing uses another let to create a new binding; mutation assigns to an existing mutable binding.
Interview answer:
I would correct the second half. Rust bindings are immutable by default, and mut explicitly enables assignment to that binding. Shadowing is different: another let creates a new binding with the same name, and the new binding takes precedence for subsequent name resolution. The difference is observable because shadowing can change the type, while assignment through mut must remain type-compatible with the existing binding. (Rust Documentation)
Deep dive:
The chapter’s spaces example is a good proof of the distinction. Shadowing allows the name to move from a string value to a numeric value; assigning through mut does not.
Example:
#![allow(unused)]
fn main() {
let spaces = " ";
let spaces = spaces.len(); // shadowing
}
Common pitfall:
Thinking that reusing the same name necessarily means the same variable is being changed.
Possible follow-up:
- What single code example best demonstrates the difference?
- Does shadowing require
mut?
Constants
Q35
Question:
What is a Rust const, and how does it differ from an ordinary immutable let binding?
Short answer:
A const is an always-immutable named value declared with const. Unlike let, it requires an explicit type annotation and its initializer must be a constant expression.
Interview answer:
The chapter identifies several differences. A constant cannot be declared with mut; it is always immutable. Its type must be explicitly annotated, whereas let can use type inference. Its initializer must also be a constant expression rather than something that can only be computed at runtime. Constants can be declared in any scope, including global scope. (Rust Documentation)
Deep dive:
The key point is that const is not merely “a let that nobody intends to modify.” It has additional language-level restrictions concerning mutability, typing, and constant evaluation.
Example:
#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100;
}
Common pitfall:
Describing const as simply an immutable let binding and omitting its additional restrictions.
Possible follow-up:
- Why must a constant have an explicit type?
- What makes a constant initializer valid?
Q38
Question:
Why must a constant have an explicit type annotation? How does this differ from a let binding?
Short answer:
Rust requires every const declaration to specify its type explicitly. A let binding can normally have its type inferred from its initializer and surrounding type information.
Interview answer:
The explicit type on a constant is a language rule: a const declaration must include its type annotation. With let, the compiler can infer the binding’s type, so let x = 5; does not need an annotation. This gives const a stricter declaration form than an ordinary let binding. (Rust Documentation)
Deep dive:
The important interview point is not to invent a deeper rationale that the chapter does not establish. The correct distinction is simply that explicit typing is mandatory for const and not mandatory for ordinary let.
Example:
#![allow(unused)]
fn main() {
let x = 5;
const X: u32 = 5;
}
Common pitfall:
Thinking that the type annotation on const is merely stylistic.
Possible follow-up:
- Can a
constomit its type if the initializer makes the type obvious? - How does this compare with type inference for
let?
Q39
Question:
What does Rust mean when it requires a const initializer to be a constant expression? Why can’t an arbitrary runtime computation be used?
Short answer:
The initializer must be an expression that Rust can evaluate during compilation. An expression whose result can only be determined while the program is running therefore cannot be used as a const initializer.
Interview answer:
A constant expression is one that satisfies Rust’s rules for compile-time evaluation. For a const, the compiler must be able to determine the initializer’s value during compilation rather than depending on runtime state or a computation that is only available when the program executes. This is why ordinary runtime-dependent computations cannot be used to initialize a constant. The chapter specifically contrasts a constant expression with “the result of a value that could only be computed at runtime.” (Rust Documentation)
Deep dive:
The important distinction is not merely whether the result happens to remain unchanged at runtime. A let binding can hold a value that never changes, but that does not make its initializer a constant expression. The restriction is about compile-time evaluation.
Example:
#![allow(unused)]
fn main() {
const THREE_HOURS: u32 = 60 * 60 * 3;
}
The compiler can evaluate the expression during compilation.
Common pitfall:
Equating “constant” with “a value that doesn’t change.” For const, the initializer must also satisfy the language’s constant-evaluation rules.
Possible follow-up:
- Why is
60 * 60 * 3allowed? - What would make an initializer runtime-dependent?
Q40
Question:
Why is this a valid constant declaration even though its initializer performs arithmetic?
#![allow(unused)]
fn main() {
const THREE_HOURS: u32 = 60 * 60 * 3;
}
What property of the expression makes it suitable for a const initializer?
Short answer:
The arithmetic uses values and operations that Rust can evaluate at compile time, so the entire initializer is a valid constant expression.
Interview answer:
A constant expression does not have to be a single literal. Rust supports a defined set of compile-time-evaluable operations, including the arithmetic in this example. The compiler can determine the result of 60 * 60 * 3 without waiting for runtime state, so the expression satisfies the requirement for a const initializer. (Rust Documentation)
Deep dive:
The chapter uses this example to show why constants can be written in a readable form such as 60 * 60 * 3 rather than as the opaque literal 10_800.
Example:
#![allow(unused)]
fn main() {
const THREE_HOURS: u32 = 60 * 60 * 3;
}
Common pitfall:
Thinking constant expressions must contain only a single literal with no operators.
Possible follow-up:
- What property would cause a computation to be unsuitable for a
constinitializer? - Why might
60 * 60 * 3be preferable to10_800?
Q41
Question:
Where can constants be declared, and how should you distinguish the lexical scope of a constant’s name from the fact that its value is valid for the duration of the program? Why does that distinction matter when discussing whether constants are “global”?
Short answer:
Constants can be declared in any scope, including global scope. Their name is usable only within the scope where the constant is declared, while the chapter describes the constant as being valid for the entire time the program runs within that scope.
Interview answer:
“Global” mixes together two different ideas. A constant may be declared in global scope, but constants can also be declared in narrower scopes. The chapter says constants are valid for the entire time a program runs, within the scope in which they were declared. So the program-wide validity does not mean that every constant name is visible everywhere. A function-local constant, for example, is still restricted by its lexical scope. (Rust Documentation)
Deep dive:
This is best explained without making claims about storage or inlining. The useful distinction is simply between how long the constant is valid and where its name is in scope.
Example:
#![allow(unused)]
fn main() {
fn f() -> u32 {
const LIMIT: u32 = 100;
LIMIT
}
}
LIMIT is declared in the function’s scope; it is not a globally visible name.
Common pitfall:
Assuming that every const is global because constants can exist for the entire program.
Possible follow-up:
- Can a constant be declared inside a function?
- What determines whether a constant’s name can be used at a particular location?
Q43
Question:
What characteristics of a value make const a better fit than an ordinary variable binding? Give examples of the kind of program-level value that should be expressed as a constant.
Short answer:
A const is appropriate for a named value that is fixed as part of the program and can be represented by a constant expression, especially when multiple parts of the program need that value.
Interview answer:
The chapter gives domain-level examples such as a maximum number of points in a game or the speed of light. These are values that have a stable meaning in the program, are not intended to vary at runtime, and can be expressed as constant expressions. Giving them a name also makes the code clearer and provides one place to update the value if its definition changes. (Rust Documentation)
Deep dive:
The important distinction is between a value being stable in one execution and being a suitable language-level constant. A runtime-computed value can remain unchanged after computation but still does not qualify as a const if its initializer is not a constant expression.
Example:
#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}
Common pitfall:
Choosing const solely because a value “doesn’t change after initialization.” The initializer must also satisfy the constant-expression rule.
Possible follow-up:
- Could a value read from a configuration file at startup be a
const? - Why is naming domain values with constants useful?
Q44
Question:
What naming convention does Rust use for constants, and what is the purpose of that convention?
Short answer:
Rust conventionally names constants in SCREAMING_SNAKE_CASE, such as MAX_POINTS. The convention makes constants visually distinguishable from ordinary variables.
Interview answer:
The Rust style convention is all uppercase with underscores between words. For example, THREE_HOURS_IN_SECONDS follows the convention shown in the chapter. It is a naming convention rather than the semantic rule that makes something a constant; the compiler’s behavior comes from the const declaration and its associated restrictions. (Rust Documentation)
Deep dive:
The naming convention helps readers recognize that a name denotes a constant without having to inspect its declaration immediately.
Example:
#![allow(unused)]
fn main() {
const MAX_RETRIES: u32 = 3;
}
Common pitfall:
Thinking SCREAMING_SNAKE_CASE is what makes something a constant. The const keyword does that; the naming style is conventional.
Possible follow-up:
- Does the compiler require this naming convention?
- Why is a naming convention useful for constants?
Q47
Question:
An interviewer says: “A const is basically a global immutable variable.” What is incomplete or misleading about that explanation?
Short answer:
Constants are not necessarily global, and they have additional language rules beyond ordinary immutable let bindings: they are always immutable, require an explicit type, and require a constant-expression initializer.
Interview answer:
The “global” part is wrong as a definition because constants can be declared in any scope. Calling a const merely an immutable variable also misses several enforced differences: const cannot be combined with mut, its type must be written explicitly, and its initializer must be a constant expression. The chapter also notes that constants are valid for the entire time a program runs within their declared scope. (Rust Documentation)
Deep dive:
A good interview description is therefore semantic rather than location-based: a const is an always-immutable named value with an explicitly declared type and a compile-time-evaluable initializer.
Example:
#![allow(unused)]
fn main() {
fn f() -> u32 {
const LIMIT: u32 = 100;
LIMIT
}
}
Common pitfall:
Equating “constant” with “global variable.”
Possible follow-up:
- What restrictions distinguish
constfromlet? - Can a function-local constant be useful?
Q49
Question:
Can a const be “transformed” the way a shadowed let binding can? Why or why not?
Short answer:
Not in the runtime transformation pattern shown by the chapter. Shadowed let bindings can be successively initialized from earlier values, while a const initializer must itself be a valid constant expression.
Interview answer:
The chapter’s shadowing pattern is useful for successive transformations such as:
#![allow(unused)]
fn main() {
let x = value;
let x = transform(x);
}
The new let can use the previous value when the statement executes. A const has a stricter rule: its initializer must be a constant expression that can be evaluated at compile time. So a constant cannot participate in an arbitrary runtime transformation pipeline in the same way a shadowed let binding can. (Rust Documentation)
Deep dive:
This answer should stay at the level established by the chapter. The chapter does not use const to demonstrate shadowing, so questions about the exact interaction between const declarations and name shadowing should be treated separately as broader Rust-language questions.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;
}
This is the chapter’s shadowing pattern. The analogous const initializer would need to satisfy constant-expression rules.
Common pitfall:
Thinking const is simply an immutable version of let, with the same initializer semantics.
Possible follow-up:
- What makes the
lettransformation possible? - What additional restriction applies to a
constinitializer?
Integrated Reasoning and Edge Cases
Q45
Question:
Compare these declarations:
#![allow(unused)]
fn main() {
let x = 60 * 60 * 3;
const X: u32 = 60 * 60 * 3;
}
What are the important semantic differences between them beyond their naming and syntax?
Short answer:
The let binding uses type inference and could be made mutable with mut; the const is always immutable, requires an explicit type, and requires a constant-expression initializer. In this particular example, both initializers satisfy the constant-expression rule.
Interview answer:
The two declarations differ in several ways. The let binding’s type is inferred; in this standalone example the unconstrained integer literal will default to an appropriate integer type, while a const requires its type to be explicitly written. The let binding is immutable only because mut was omitted, whereas a const is always immutable. Finally, a const initializer must satisfy Rust’s constant-expression rules; the arithmetic here qualifies, but a let initializer can generally be a runtime computation instead. (Rust Documentation)
Deep dive:
The important point is that the two declarations happen to produce similarly stable values here, but they express different language constructs with different restrictions.
Example:
#![allow(unused)]
fn main() {
let x = 60 * 60 * 3;
const X: u32 = 60 * 60 * 3;
}
Common pitfall:
Saying “both are constants because neither changes.” An immutable let binding and a const are not the same language construct.
Possible follow-up:
- What changes if
let xbecomeslet mut x? - Which initializer could depend on runtime input?
Q31
Question:
Suppose a value must be updated repeatedly as part of a loop. Why would a mutable binding generally be a more natural representation than repeatedly shadowing the value?
Short answer:
A mutable binding can persist across loop iterations and be assigned repeatedly. A binding created inside the loop body by shadowing is scoped to that iteration’s block and does not itself provide persistent state for the next iteration.
Interview answer:
For a running accumulator, the natural model is one binding whose value changes over time:
#![allow(unused)]
fn main() {
let mut total = 0;
for i in 1..=5 {
total += i;
}
}
Here total is the same binding throughout the loop. By contrast, a let total = ... inside the loop body creates a new binding for that iteration. That binding does not replace the outer accumulator for future iterations; it is limited by the loop body’s scope.
Deep dive:
This illustrates an important design distinction. Shadowing is convenient when you’re moving from one transformation stage to another, while mut is more natural when the same binding represents evolving state over repeated operations.
Example:
#![allow(unused)]
fn main() {
let mut total = 0;
for i in 1..=5 {
total += i;
}
}
Common pitfall:
Assuming a shadowing declaration inside the loop automatically carries its value into the next iteration.
Possible follow-up:
- What happens if
let mut total = 0is placed inside the loop? - When might shadowing still be useful inside a loop?
Q58
Question:
In this statement, which x is used to evaluate the right-hand side?
#![allow(unused)]
fn main() {
let x = x + 1;
}
Assuming an x from an enclosing scope already exists, explain why the existing binding can be used to initialize the new binding with the same name.
Short answer:
The right-hand side uses the existing x. The new binding introduced by the let is not the binding being referenced by its own initializer.
Interview answer:
The initializer is evaluated using the name resolution that exists before the new binding takes effect. With an existing x in an enclosing scope, x + 1 therefore reads that existing binding. The result is then used to initialize the new binding named x. This is exactly the pattern illustrated by the chapter’s let x = x + 1; example. (Rust Documentation)
Deep dive:
A useful way to reason about it is that the initializer needs an already-resolvable x. The new shadowing binding does not retroactively change what x meant while its initializer was being evaluated.
Example:
#![allow(unused)]
fn main() {
let x = 5;
let x = x + 1;
println!("{x}"); // 6
}
Common pitfall:
Treating the statement as an attempted self-reference to the new binding.
Possible follow-up:
- What happens if no earlier
xexists? - How does this relate to shadowing in the chapter’s examples?
Q50
Question:
Can a const declaration participate in name shadowing in the same way a local let binding does? This is not stated directly by the chapter. Explain how you would reason about the question from Rust’s rules for names, scopes, and shadowing, and distinguish what the chapter establishes from what you are extrapolating.
Short answer:
The chapter does not establish the answer. It explicitly demonstrates shadowing for let bindings, but not for const declarations, so this should be treated as an extrapolation that requires verification rather than as a chapter fact.
Interview answer:
I would separate what the chapter establishes from what I know more generally about Rust. This chapter explicitly explains shadowing by repeating let and demonstrates how lexical scope affects those bindings. It does not state whether the same shadowing rules apply to const declarations. Therefore, from this chapter alone, I would not claim a definitive answer. I would verify the behavior against the Rust Reference or a compiler before presenting it as fact.
Deep dive:
This is a useful interview distinction: knowing a nearby rule is not the same as having established that the rule applies to a different declaration kind. A strong answer should clearly label the boundary between chapter-derived knowledge and extrapolation.
Example:
Not applicable.
Common pitfall:
Confidently presenting an inferred rule about const shadowing as though the chapter explicitly stated it.
Possible follow-up:
- How would you verify the behavior?
- Which part of the chapter is insufficient to establish the answer?
Q59
Question:
State the type-consistency guarantee Rust gives you for a mut binding, and explain why that guarantee does not prevent a subsequent shadowing let from giving the same name a different type.
Short answer:
Assignments through one mut binding must remain type-compatible with that binding’s established type. A shadowing let creates a different binding, so it can establish a different type for the same name.
Interview answer:
The type guarantee is binding-specific. Once a mut binding has been initialized, later assignments to that binding must produce values of the same type; the spaces example demonstrates this when assigning a usize to a binding initially holding a string slice. A subsequent let is not an assignment to that binding. It creates a new binding with the same name, and the new binding has its own type. Therefore there is no violation of static typing: the original binding keeps its original type, while the new binding establishes another. (Rust Documentation)
Deep dive:
The causal chain is:
mut → assignment to the same binding → same type.
Shadowing is:
let → new binding → independently determined type.
That is why shadowing does not weaken Rust’s type system.
Example:
#![allow(unused)]
fn main() {
let mut x = 5;
// x = "now a string"; // type mismatch
let x = "now this is a new binding";
}
Common pitfall:
Saying that Rust “allows a variable to change type when shadowed.” More precisely, the name is reused for a different binding with a different type.
Possible follow-up:
- Would the new shadowing binding itself be mutable?
- Does the original
mutbinding’s type change at any point?
Q53
Question:
What kind of bug does Rust’s default immutability help prevent in practice? Give a concrete example of why making a binding immutable unless mutation is intentional can improve reasoning about code.
Short answer:
It prevents bugs where one part of the code assumes a value does not change while another part accidentally changes it. Without mut, the compiler enforces that the binding cannot be reassigned.
Interview answer:
The chapter gives exactly this class of bug as a motivation for compile-time immutability. Imagine one part of a function assumes a value remains fixed, while another part conditionally assigns a new value to it. That creates behavior that can be difficult to reason about because the assumption made by the first part is no longer reliable. Requiring mut makes the possibility of reassignment explicit and lets the compiler reject accidental assignments to bindings that were intended to remain unchanged. (Rust Documentation)
Deep dive:
The important benefit is that this is a language-enforced guarantee rather than a convention. The chapter emphasizes that this makes code easier to reason about because the programmer does not need to track possible assignments manually. (Rust Documentation)
Example:
#![allow(unused)]
fn main() {
let threshold = 100;
// later code can rely on this binding not being reassigned
if value > threshold {
println!("over threshold");
}
}
Common pitfall:
Describing immutability only as a style preference. In Rust, the compiler enforces the restriction.
Possible follow-up:
- Why is compile-time enforcement more useful than relying on comments or conventions?
- What explicit signal tells a reader that a binding is intended to change?
Q54
Question:
Suppose an interviewer asks, “Why not make everything mutable and rely on developers not to change values unless necessary?” What aspect of Rust’s design philosophy is being tested, and how would you answer?
Short answer:
Rust favors explicit, compiler-enforced guarantees over relying solely on programmer discipline. Making immutability the default turns “this should not change” into a property the compiler can enforce.
Interview answer:
The question tests Rust’s preference for making important invariants explicit and enforceable at compile time. If everything were mutable, avoiding mutation would be a convention that developers would have to maintain manually. With immutability by default, accidental assignments become compiler errors, and mut communicates when mutation is intentional. The chapter presents this as a way to make code easier to reason about and to prevent bugs caused by conflicting assumptions about whether a value changes. (Rust Documentation)
Deep dive:
The trade-off is that developers sometimes have to write mut explicitly even when mutation is straightforward, but that extra annotation is also the design signal: readers can immediately see where mutation is intended.
Example:
#![allow(unused)]
fn main() {
let value = 5;
// value = 6; // rejected
let mut changing_value = 5;
changing_value = 6; // explicitly permitted
}
Common pitfall:
Answering only “Rust is safer” without explaining the mechanism. The important point is that the desired invariant is made compiler-checkable.
Possible follow-up:
- What benefit does the
mutkeyword provide to human readers? - What trade-off does immutable-by-default design introduce?