When I first started learning Rust, I assumed naming a variable would be the easiest part of the language. I was wrong. Rust has strong opinions about how you name and declare things, and those opinions exist for a very good reason: memory safety. In this article, I want to walk you through everything I’ve learned about naming objects in Rust — variables, constants, shadowing, and the naming conventions the community actually follows in production code.
By the end, you’ll understand not just the “how” but the “why” behind Rust’s approach to naming, and how it ties directly into the ownership and safety guarantees that make Rust special.
Why Naming Matters More in Rust Than in Other Languages
In most languages, a variable is just a label pointing to a value. You declare it, you reassign it, you move on. Rust treats variable declarations as a contract. When I write let x = 5;, I’m not just creating a name — I’m making a promise to the compiler about mutability, ownership, and lifetime. Getting the naming and declaration semantics right up front saves you from a mountain of borrow-checker errors later.
Declaring Variables with let
Every variable in Rust starts life with the let keyword.
fn main() {
let age = 30;
println!("My age is {}", age);
}
Output:
My age is 30
Here’s the twist that trips up beginners coming from JavaScript or Python: variables in Rust are immutable by default. If I try this:
fn main() {
let age = 30;
age = 31; // this will not compile
}
I get a compiler error:
error[E0384]: cannot assign twice to immutable variable `age`
This isn’t a bug or an inconvenience — it’s one of Rust’s core memory safety principles. By defaulting to immutability, Rust forces me to be explicit about which values are allowed to change, which makes it much easier to reason about a program, especially when multiple parts of the code (or multiple threads) might touch the same data.
Making Variables Mutable
When I actually need a variable to change, I add the mut keyword:
fn main() {
let mut age = 30;
println!("Before birthday: {}", age);
age = 31;
println!("After birthday: {}", age);
}
Output:
Before birthday: 30
After birthday: 31
I like to think of mut as a visible flag in the code that says “this value’s state will change — pay attention here.” When I’m reviewing someone else’s Rust code, seeing mut immediately tells me where mutation happens, which narrows down where bugs could hide.
Constants: Values That Never Change
Constants are declared with the const keyword, and they’re stricter than immutable variables:
const MAX_USERS: u32 = 1000;
fn main() {
println!("Maximum allowed users: {}", MAX_USERS);
}
A few rules I had to internalize about constants:
- You must always annotate the type (
u32above) — type inference doesn’t apply. - Constants can be declared in any scope, including the global scope, unlike
letvariables. - Constants must be set to a value that can be computed at compile time, not something calculated at runtime.
- By convention, constant names are written in
SCREAMING_SNAKE_CASE.
I use constants for values that represent fixed facts about my program — things like maximum buffer sizes, mathematical constants, or configuration ceilings that will never change during execution.
const vs Immutable let
This confused me for a while, so let me clarify it plainly:
| Feature | let (immutable) | const |
|---|---|---|
| Can be reassigned | No | No |
| Requires type annotation | No (inferred) | Yes (mandatory) |
| Scope | Block-scoped | Any scope, including global |
| Evaluated at | Runtime | Compile time |
| Can shadow | Yes | No (in the same sense) |
Shadowing: Rust’s Elegant Alternative to Mutation
Shadowing is one of my favorite features in Rust, and it’s something a lot of beginners overlook. Shadowing lets me declare a new variable with the same name as a previous one, effectively “hiding” the old one.
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("Number of spaces: {}", spaces);
}
Output:
Number of spaces: 3
Notice what happened: spaces started as a &str and ended as a usize. That’s only possible because I created a brand-new variable, not because I mutated the old one. If I had used mut instead, this code would not compile, because you can’t change a variable’s type through mutation.
I find shadowing especially useful for transforming a value step-by-step without inventing a new name at every stage:
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The final value of x is: {}", x);
}
Output:
The final value of x is: 12
Each let creates a completely new binding. The old x values still technically exist in memory until they go out of scope, but the name x now refers to the newest one. This is different from mutability because I can also change the type, and because each shadowed variable can carry its own immutability by default.
Shadowing Inside Blocks
Shadowing also respects scope, which gives me fine control over temporary transformations:
fn main() {
let y = 5;
{
let y = y * 2;
println!("Inner scope y: {}", y);
}
println!("Outer scope y: {}", y);
}
Output:
Inner scope y: 10
Outer scope y: 5
Once the inner block ends, the shadowed y disappears, and the outer y is untouched. This is a direct reflection of how Rust manages memory through scope-based ownership — when a variable goes out of scope, Rust automatically cleans it up, and shadowed variables are no exception.
Naming Conventions in Rust
Rust has an official style guide (enforced loosely by the compiler and strictly by the community and tools like clippy and rustfmt). Here’s what I follow:
- Variables and functions:
snake_case— e.g.,user_age,calculate_total. - Constants and statics:
SCREAMING_SNAKE_CASE— e.g.,MAX_CONNECTIONS. - Types, structs, enums, and traits:
PascalCase— e.g.,UserAccount,HttpRequest. - Modules and crates:
snake_case, usually short and lowercase — e.g.,mod parser.
If I break these conventions, the compiler will actually issue a warning:
warning: variable `UserAge` should have a snake case name
This is Rust nudging me toward consistency across the entire ecosystem — one reason Rust code from different authors tends to look remarkably similar.
Naming and Ownership: The Deeper Connection
Here’s something that took me a while to appreciate: naming in Rust is tightly connected to ownership. When I write:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // error!
}
I get a compile error because s1‘s ownership moved to s2. The name s1 still exists syntactically, but the compiler considers the binding invalid after the move. This is the essence of Rust’s memory safety model — every value has exactly one owner at a time, and naming a variable is really about creating (or transferring) that ownership relationship, not just creating a label.
Common Mistakes I Made as a Beginner
- Forgetting
mut: I’d writelet count = 0;and then try to increment it in a loop, only to get hit with an immutability error. - Overusing shadowing: Shadowing is powerful, but overusing it in long functions can make code harder to follow. I now shadow mainly for short, clear transformations.
- Confusing
constwithstatic:staticvariables have a fixed memory address for the entire program duration and can (in unsafe contexts) be mutable, whileconstvalues are inlined wherever they’re used. I stick withconstunless I specifically need a stable memory address. - Non-idiomatic casing: Coming from other languages, I initially wrote
camelCasevariables out of habit.rustfmtandclippyquickly corrected that instinct.
Best Practices I Follow Now
- Default to immutability, and only add
mutwhen the compiler (or logic) demands it. - Use descriptive,
snake_casenames — Rust’s readability shines when names are clear. - Reserve constants for truly fixed, compile-time-known values.
- Use shadowing for transformation pipelines, not for unrelated values that happen to share a name.
- Run
cargo fmtandcargo clippyregularly — they catch naming issues before they become habits.
Troubleshooting and FAQs
Q: Why does Rust make variables immutable by default? It’s a deliberate design choice tied to memory safety. Immutability by default reduces accidental state changes, which is one of the most common sources of bugs in concurrent or long-running programs.
Q: Can I shadow a variable with a different type? Yes. Shadowing creates a new binding, so the type can differ entirely from the original variable, as I showed with the spaces example above.
Q: What happens to the old value when I shadow a variable? It stays in memory until it goes out of scope, at which point Rust’s ownership system cleans it up automatically — no garbage collector required.
Q: Why do I get a “cannot assign twice” error even though I only assigned once? This usually happens when you’re re-declaring with = instead of shadowing with a new let. Double-check whether you meant to use let again.
Q: Is there a performance cost to shadowing? No. Shadowing is a compile-time concept. Once compiled, there’s no runtime overhead compared to just using multiple names.
Summary
Naming objects in Rust isn’t just cosmetic — it’s woven into the language’s memory safety guarantees. Immutability by default protects you from accidental mutation, mut makes intentional mutation explicit and visible, constants give you compile-time guarantees for fixed values, and shadowing offers an elegant way to transform data without fighting the type system. Once these concepts clicked for me, a lot of the Rust compiler’s “strictness” started to feel like a safety net rather than an obstacle.
If you’re just starting out, spend time experimenting with each of these concepts in the Rust Playground. Try to break them, read the compiler errors carefully, and you’ll build an intuition for Rust’s ownership model faster than you’d expect.
References
- The Rust Programming Language Book, official documentation: https://doc.rust-lang.org/book/
- Rust Reference on Variables and Constants: https://doc.rust-lang.org/reference/
- Rust API Guidelines on Naming: https://rust-lang.github.io/api-guidelines/
- Cargo Documentation: https://doc.rust-lang.org/cargo/