KAIROS CODERS

Rust Variables, Mutability and Constants: A Complete Beginner's Guide

user

Rahul

August 25, 2026 at 06:59 PM

View Count: 7

Rust Variables, Mutability and Constants: A Complete Beginner's Guide

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:

  • Variables are immutable by default.
  • Mutability must be explicitly requested.
  • Variables can be shadowed.
  • Constants are different from variables.
  • Rust has strict rules around types.
  • Scope determines where a variable can be accessed.

Understanding these concepts early will make later topics such as ownership, borrowing, lifetimes, and concurrency much easier.


What Is a Variable?

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 → 30

The variable gives your program a convenient name through which it can work with the value.


Creating Variables With let

Rust 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.


Variables Are Immutable by Default

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.


Making a Variable Mutable

If you want to change a variable, use mut.

fn main() {
    let mut age = 30;

    age = 31;

    println!("{age}");
}

Output:

31

The syntax is:

let mut variable = value;

The keyword mut means:

This binding is allowed to be modified.


Why Is Rust Immutable by Default?

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

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:

30

The same variable is being modified several times.


You Cannot Change a Variable's Type Through Mutation

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.


Shadowing

Rust allows you to declare another variable with the same name.

fn main() {
    let value = 10;

    let value = 20;

    println!("{value}");
}

Output:

20

The second value shadows the first one.

This is called shadowing.


Shadowing vs Mutation

These two concepts may look similar, but they are fundamentally different.

Mutation

let mut value = 10;

value = 20;

You're changing the value of an existing mutable binding.

Shadowing

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.


Shadowing Can Change the Type

This is one of the most useful properties of shadowing.

For example:

fn main() {
    let spaces = "   ";

    let spaces = spaces.len();

    println!("{spaces}");
}

Initially:

spaces → string

After shadowing:

spaces → integer

This 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.


Why Is Shadowing Useful?

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.


Shadowing Happens in Order

Consider:

fn main() {
    let number = 10;

    println!("{number}");

    let number = 20;

    println!("{number}");
}

Output:

10
20

The first binding exists before the second declaration.

After the second declaration, the newer binding shadows the older one.


Scope

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.


Understanding Scope Visually

Think of the program like this:

main scope
│
├── message
│
└── inner scope
    │
    └── name

The 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.


Why Scope Matters

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.


Constants

Rust also supports constants.

A constant is declared using:

const

For example:

const MAX_USERS: u32 = 1000;

Unlike ordinary variables, constants:

  • Must have an explicit type.
  • Cannot be mutable.
  • Must be initialized with a constant expression.
  • Can be declared in global scope.
  • Are intended for values that conceptually never change.

Creating a Constant

Example:

const MAX_LOGIN_ATTEMPTS: u32 = 5;

fn main() {
    println!("{MAX_LOGIN_ATTEMPTS}");
}

Output:

5

The type is explicitly specified:

u32

Rust requires this for constants.


Constants Cannot Be Mutable

This is invalid:

const mut MAX_USERS: u32 = 1000;

Constants are inherently immutable.

You don't use mut with const.


Constants Usually Use SCREAMING_SNAKE_CASE

Rust's conventional naming style for constants is:

SCREAMING_SNAKE_CASE

Examples:

const MAX_USERS: u32 = 1000;
const DATABASE_TIMEOUT: u64 = 30;
const COMPANY_NAME: &str = "Kairos Coders";

This makes constants immediately recognizable.


Variable vs Constant

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.


When Should You Use 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.


static

Rust 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.


Type Annotations

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.


Type Inference

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.


Explicit Types Can Be Helpful

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.


Integer Types

Rust provides several integer types.

Signed integers:

i8
i16
i32
i64
i128
isize

Unsigned integers:

u8
u16
u32
u64
u128
usize

For 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.


Floating-Point Types

Rust provides:

f32
f64

For example:

let temperature: f32 = 36.5;
let price: f64 = 999.99;

f64 is commonly used when you need general-purpose floating-point calculations.


Boolean Variables

A Boolean can contain either:

true

or:

false

Example:

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.


Character Variables

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.


Variable Names

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.


Rust Naming Convention

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.


Variable Shadowing in Real Code

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 input

Shadowing allows the variable name to remain meaningful throughout the transformation.


Mutation in Real Code

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: 3

Here mutation makes sense because the counter is expected to change.


Compound Assignment Operators

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.


Scope and Shadowing Together

Consider:

fn main() {
    let value = 10;

    {
        let value = 20;

        println!("Inner: {value}");
    }

    println!("Outer: {value}");
}

Output:

Inner: 20
Outer: 10

The inner variable shadows the outer variable only within its scope.

Once the inner scope ends, the outer value becomes visible again.


A Practical Example: Shopping Cart

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:

  • A constant
  • Immutable variables
  • A mutable variable
  • A function
  • Floating-point values
  • Arithmetic
  • Scope

These concepts will appear constantly in real Rust applications.


A Common Mistake: Using mut Everywhere

Beginners 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.


A Common Mistake: Confusing Shadowing With Mutation

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


A Common Mistake: Using a Variable Outside Its Scope

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.


A More Realistic Example

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:

  • Constant
  • Immutable variable
  • Mutable variable
  • Boolean
  • Integer
  • String literal
  • Mutation
  • Formatted output

Variable Best Practices

When writing Rust, follow these principles:

1. Prefer immutable variables

let value = 100;

Use mutation only when necessary.

2. Use meaningful names

let account_balance = 50000;

3. Use constants for fixed values

const MAX_RETRIES: u32 = 3;

4. Use shadowing when transforming values

let input = input.trim();

5. Keep scopes small

Avoid unnecessarily large scopes.

6. Let Rust infer types when they're obvious

Instead of:

let age: i32 = 30;

you can often write:

let age = 30;

Use explicit types when they improve clarity or are required.


Practice Exercises

Exercise 1 — Mutable Counter

Create a variable:

counter = 0

Increment it five times and print the result.

Expected output:

5

Exercise 2 — Constants

Create constants for:

PI
MAX_USERS
APP_NAME

Print all three.


Exercise 3 — Shadowing

Create:

let number = 10;

Then shadow it with a string containing:

"Rust"

Print the final value.


Exercise 4 — Temperature

Create a Celsius temperature and convert it into Fahrenheit.

Use:

F = C × 9/5 + 32

Exercise 5 — Shopping Bill

Create:

  • Product price
  • Quantity
  • Tax rate constant

Calculate:

subtotal
tax
final total

Print all three.


What You Learned

In this article, you learned some of the most fundamental Rust concepts:

  • let
  • mut
  • Immutable variables
  • Mutable variables
  • Shadowing
  • Scope
  • Constants
  • const
  • static
  • Type inference
  • Type annotations
  • Integer types
  • Floating-point types
  • Boolean values
  • Characters
  • Compound assignment
  • Variable naming
  • Rust naming conventions

These concepts might seem basic, but they become critical when we reach Rust's ownership system.

Pixels to Perfection Design that Impresses

Want to partner with us? let's innovate together