In the previous article, we wrote our first Rust programs and learned about functions, expressions, statements, println!, and basic syntax.
Now we're going to explore one of the most fundamental concepts in every programming language:
Variables.
Variables allow programs to store and work with information.
But Rust approaches variables differently from many beginner-friendly languages.
In Rust:
Understanding these concepts early will make later topics such as ownership, borrowing, lifetimes, and concurrency much easier.
A variable is a named binding to a value.
For example:
fn main() {
let age = 30;
println!("{age}");
}Here:
let age = 30;creates a variable called age.
You can think of it conceptually as:
age → 30The variable gives your program a convenient name through which it can work with the value.
letRust uses the let keyword to create variables.
let name = "Rahul";
let age = 30;
let salary = 50000.0;A general variable declaration looks like:
let variable_name = value;For example:
let country = "India";The compiler can usually determine the type of the value automatically.
This is called type inference.
This is one of the most important things to understand about Rust.
Consider:
fn main() {
let age = 30;
age = 31;
}This program will not compile.
Why?
Because age is immutable.
Rust assumes that a variable should not change unless you explicitly say otherwise.
If you want to change a variable, use mut.
fn main() {
let mut age = 30;
age = 31;
println!("{age}");
}Output:
31The syntax is:
let mut variable = value;The keyword mut means:
This binding is allowed to be modified.
This design encourages safer and more predictable code.
Imagine a large program containing:
let account_balance = 50000;If variables were automatically mutable, any part of the code with access to that binding could potentially change it.
Rust instead requires you to explicitly write:
let mut account_balance = 50000;This communicates your intention.
When reading Rust code, seeing mut tells another developer:
This value is expected to change.
That small distinction becomes increasingly valuable in large applications.
Mutation means changing the value associated with a mutable variable.
fn main() {
let mut score = 0;
score = 10;
score = 20;
score = 30;
println!("{score}");
}Output:
30The same variable is being modified several times.
Consider:
fn main() {
let mut value = 10;
value = "Rust";
}This doesn't compile.
Why?
The variable was initially inferred as an integer.
Mutation changes the value, not the type of the existing binding.
You could instead use shadowing.
Rust allows you to declare another variable with the same name.
fn main() {
let value = 10;
let value = 20;
println!("{value}");
}Output:
20The second value shadows the first one.
This is called shadowing.
These two concepts may look similar, but they are fundamentally different.
let mut value = 10;
value = 20;You're changing the value of an existing mutable binding.
let value = 10;
let value = 20;You're creating a new binding that happens to have the same name.
This distinction becomes especially useful when transforming data.
This is one of the most useful properties of shadowing.
For example:
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("{spaces}");
}Initially:
spaces → stringAfter shadowing:
spaces → integerThis is perfectly valid.
Mutation would not allow this:
let mut spaces = " ";
spaces = spaces.len();The types don't match.
Shadowing creates a new binding, so the new binding can have a different type.
Suppose you're processing some input.
You might start with:
let input = "42";Then convert it:
let input = input.parse::<i32>().unwrap();Now input represents an integer.
This can make code easier to read because you don't need meaningless names such as:
let input_string = "42";
let input_number = input_string.parse::<i32>().unwrap();Both approaches can be valid, but shadowing allows the same conceptual value to retain a meaningful name as its representation changes.
Consider:
fn main() {
let number = 10;
println!("{number}");
let number = 20;
println!("{number}");
}Output:
10
20The first binding exists before the second declaration.
After the second declaration, the newer binding shadows the older one.
A variable doesn't necessarily exist everywhere in a program.
Its accessibility depends on its scope.
For example:
fn main() {
let message = "Hello";
{
let name = "Rust";
println!("{message}");
println!("{name}");
}
println!("{message}");
}The outer variable message is available inside the inner block.
But name only exists inside the inner block.
Think of the program like this:
main scope
│
├── message
│
└── inner scope
│
└── nameThe inner scope can access values from the outer scope.
But the outer scope cannot access variables created inside the inner scope.
For example:
fn main() {
{
let name = "Rust";
}
println!("{name}");
}This won't compile because name no longer exists outside its scope.
Scope is extremely important in Rust because it connects directly to ownership.
Later, you'll learn that Rust automatically determines when values should be cleaned up based partly on scope.
For example:
{
let message = String::from("Hello");
}When this scope ends, Rust can clean up the String.
This behavior becomes one of the foundations of Rust's memory-safety model.
Rust also supports constants.
A constant is declared using:
constFor example:
const MAX_USERS: u32 = 1000;Unlike ordinary variables, constants:
Example:
const MAX_LOGIN_ATTEMPTS: u32 = 5;
fn main() {
println!("{MAX_LOGIN_ATTEMPTS}");
}Output:
5The type is explicitly specified:
u32Rust requires this for constants.
This is invalid:
const mut MAX_USERS: u32 = 1000;Constants are inherently immutable.
You don't use mut with const.
SCREAMING_SNAKE_CASERust's conventional naming style for constants is:
SCREAMING_SNAKE_CASEExamples:
const MAX_USERS: u32 = 1000;
const DATABASE_TIMEOUT: u64 = 30;
const COMPANY_NAME: &str = "Kairos Coders";This makes constants immediately recognizable.
Consider:
let max_users = 1000;versus:
const MAX_USERS: u32 = 1000;They're not interchangeable.
A let binding is a local variable binding.
A const is a compile-time constant.
The constant is appropriate when the value represents a fixed piece of program configuration or mathematical/domain knowledge.
const?Good examples include:
const MAX_RETRIES: u32 = 3;
const MAX_CONNECTIONS: usize = 100;
const TAX_RATE: f64 = 0.18;
const APP_NAME: &str = "RustApp";You wouldn't generally use constants for values that need to change during execution.
For example:
let mut counter = 0;is appropriate for a changing counter.
staticRust also has another concept called static.
For example:
static APP_NAME: &str = "Rust Application";A static value has a fixed memory location for the entire lifetime of the program.
static is an advanced concept and comes with rules that differ from const.
For normal beginner programming, prefer const when you need a named constant.
We'll revisit static when we discuss global state and advanced Rust.
Rust can infer many types automatically.
For example:
let age = 30;But you can explicitly specify the type:
let age: i32 = 30;The syntax is:
let variable: Type = value;Examples:
let age: i32 = 30;
let price: f64 = 99.99;
let active: bool = true;Explicit annotations can improve readability and are sometimes required when the compiler cannot determine the intended type.
Rust has a powerful type inference system.
For example:
let age = 30;Rust determines that the value is an integer type based on context.
Similarly:
let price = 99.99;Rust can infer a floating-point type.
You don't have to annotate every variable.
This keeps Rust code concise without sacrificing static type checking.
Suppose you are working with financial calculations:
let price: f64 = 1999.99;The explicit annotation tells readers exactly what type is intended.
Or when dealing with large identifiers:
let user_id: u64 = 123456789;Explicit types can communicate important design decisions.
Rust provides several integer types.
Signed integers:
i8
i16
i32
i64
i128
isizeUnsigned integers:
u8
u16
u32
u64
u128
usizeFor example:
let small: i8 = 10;
let normal: i32 = 1000;
let large: i64 = 100000;
let positive: u32 = 500;We'll examine these types in detail in the next article.
Rust provides:
f32
f64For example:
let temperature: f32 = 36.5;
let price: f64 = 999.99;f64 is commonly used when you need general-purpose floating-point calculations.
A Boolean can contain either:
trueor:
falseExample:
let is_logged_in = true;
let is_admin = false;You can use them in conditional logic:
if is_logged_in {
println!("Welcome!");
}We'll explore if and other control-flow constructs in detail later.
Rust also has a char type.
A character is written using single quotes:
let letter: char = 'R';Notice the difference:
'R'is a character.
While:
"R"is a string slice.
This distinction becomes important when working with text.
Rust variable names should be meaningful.
Prefer:
let user_name = "Rahul";
let account_balance = 50000;
let order_count = 10;over:
let x = "Rahul";
let a = 50000;
let n = 10;unless the shorter names have an obvious mathematical or local meaning.
Good naming improves maintainability.
Variables and functions generally use snake_case.
Correct:
let user_name = "Rahul";
fn calculate_total() {
}Avoid:
let userName = "Rahul";for normal Rust variable naming.
Rust's conventions are documented and enforced through community tooling.
Following them makes your code easier for other Rust developers to understand.
Here's a practical example:
fn main() {
let username = "rahul";
let username = username.trim();
let username = username.to_uppercase();
println!("{username}");
}Conceptually, we're transforming the same piece of data:
raw input
↓
trimmed input
↓
uppercase inputShadowing allows the variable name to remain meaningful throughout the transformation.
Mutation is appropriate when a value naturally changes.
For example, a counter:
fn main() {
let mut counter = 0;
counter += 1;
counter += 1;
counter += 1;
println!("Counter: {counter}");
}Output:
Counter: 3Here mutation makes sense because the counter is expected to change.
Rust supports operators such as:
+=
-=
*=
/=
%=For example:
let mut score = 100;
score += 10;
score -= 5;
score *= 2;
score /= 5;These are shortcuts for modifying a mutable value.
Consider:
fn main() {
let value = 10;
{
let value = 20;
println!("Inner: {value}");
}
println!("Outer: {value}");
}Output:
Inner: 20
Outer: 10The inner variable shadows the outer variable only within its scope.
Once the inner scope ends, the outer value becomes visible again.
Let's combine variables, mutation, constants, and functions.
const TAX_RATE: f64 = 0.18;
fn calculate_tax(amount: f64) -> f64 {
amount * TAX_RATE
}
fn main() {
let product_price = 1000.0;
let quantity = 2.0;
let mut subtotal = product_price * quantity;
let tax = calculate_tax(subtotal);
subtotal += tax;
println!("Final amount: ₹{subtotal}");
}Here we have:
These concepts will appear constantly in real Rust applications.
mut EverywhereBeginners sometimes write:
let mut name = "Rust";
let mut age = 30;
let mut country = "India";
let mut language = "Rust";even when nothing changes.
That's unnecessary.
Prefer:
let name = "Rust";
let age = 30;
let country = "India";
let language = "Rust";Only use mut when mutation is actually required.
This makes your intentions clearer.
These are different:
let value = 10;
let value = 20;and:
let mut value = 10;
value = 20;The first uses shadowing.
The second uses mutation.
Remember:
Shadowing → new binding
Mutation → modify existing mutable binding
This doesn't work:
fn main() {
{
let message = "Hello";
}
println!("{message}");
}The message variable belongs to the inner scope.
Its lifetime ends when that scope ends.
This concept will become extremely important when we study ownership.
Let's create a small employee information program:
const COMPANY_NAME: &str = "Kairos Coders";
fn main() {
let employee_name = "Rahul";
let mut experience = 8;
let is_active = true;
experience += 1;
println!("Company: {COMPANY_NAME}");
println!("Employee: {employee_name}");
println!("Experience: {experience} years");
println!("Active: {is_active}");
}This example demonstrates:
When writing Rust, follow these principles:
let value = 100;Use mutation only when necessary.
let account_balance = 50000;const MAX_RETRIES: u32 = 3;let input = input.trim();Avoid unnecessarily large scopes.
Instead of:
let age: i32 = 30;you can often write:
let age = 30;Use explicit types when they improve clarity or are required.
Create a variable:
counter = 0Increment it five times and print the result.
Expected output:
5Create constants for:
PI
MAX_USERS
APP_NAMEPrint all three.
Create:
let number = 10;Then shadow it with a string containing:
"Rust"Print the final value.
Create a Celsius temperature and convert it into Fahrenheit.
Use:
F = C × 9/5 + 32Create:
Calculate:
subtotal
tax
final totalPrint all three.
In this article, you learned some of the most fundamental Rust concepts:
letmutconststaticThese concepts might seem basic, but they become critical when we reach Rust's ownership system.
Pixels to Perfection Design that Impresses