Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Immutable and Mutable Bindings: Explicit Types and Type Inference

Hello! Last time, you created a Cargo project and used cargo check and cargo run to turn an edit in src/main.rs into a running program. This lesson changes what goes inside main: you will give values names, decide whether those names can be reassigned, and see how Rust determines their types.

These are small decisions, but they matter when a program starts tracking changing state—such as the number of requests currently being handled by an API. Allow about 35–40 minutes, including time to edit and run the examples.


Bind a value to a name

In Rust, let creates a binding: it associates a name with a value. The binding is immutable by default.

let connection_limit = 8;

After this declaration, you can use connection_limit, but you cannot assign a different value to it. To allow reassignment, put mut before the name:

let mut active_connections = 2;
active_connections = 3;

Watch the short demonstration in Creating variables in Rust by Rustfully. Focus on the compiler’s objection to reassignment and on the single change that permits it; the printing syntax is secondary for now.

3: Creating variables in Rust

Watch “3: Creating variables in Rust” from Rustfully to see immutable and mutable bindings compiled in a small program.

Watch the first binding for the let declaration, then the reassignment for the compiler error and the addition of mut. Stop when the discussion moves to naming conventions and constants.

Immutability is a promise enforced by the compiler, not merely a convention. If connection_limit is intended to remain eight throughout this part of the program, let makes an accidental reassignment a compilation error. For a count that must change, let mut makes that intention visible.


Infer a type or write it explicitly

Rust also needs to know the type of each binding. Often it can infer that type, so you do not need to write it:

let connection_limit = 8;
let mut active_connections = 2;

You can instead provide an annotation after the name:

let connection_limit: u32 = 8;
let mut active_connections: u32 = 2;

Here u32 is an unsigned, 32-bit integer type. The placement is the same whether the binding is mutable or immutable: let, optional mut, name, optional : Type, then = value. Mutability and type are separate choices.

Read the relevant part of Primitives in the official Rust By Example. Its compact example shows annotations, numeric suffixes, inference, and mut together. Do not paste its whole sample into your project: it deliberately includes a type error.

Primitives - Rust By Example

Read the type-annotation explanation and the first part of the example to connect Rust’s declaration syntax with type inference.

Under “Primitives,” briefly scan “Scalar Types” to locate u32, i32, and f64; leave “Compound Types” for the next lesson. Immediately below “Compound Types,” read the inference explanation. In the code sample directly beneath it, follow the binding examples, from let logical through the intentionally invalid Boolean reassignment. Notice which declarations specify a type and which let Rust determine it.

Two details make inference more useful than simply “guessing from the value”:

  • If an integer literal has no other type information, Rust defaults it to i32; a similarly unconstrained floating-point literal defaults to f64.
  • Context can determine the type instead. An annotation elsewhere in an expression may constrain a binding whose own declaration has no annotation.

For example, in the following complete program, available is inferred to be u32 because it is computed from two u32 values:

fn main() {
    let connection_limit: u32 = 8;
    let mut active_connections: u32 = 2;

    active_connections += 1;
    let available = connection_limit - active_connections;

    println!("active: {active_connections}; available: {available}");
}

The explicit annotations document the intended integer type for the two quantities we start with. The type of available is already clear from the calculation, so an annotation there adds little. Rust checks types at compile time in both cases; omitting an annotation does not make a binding dynamically typed.


Reassignment does not change a binding’s type

mut permits a new value of the binding’s established type. It does not permit a new type. For example, this is valid:

let mut attempts: u32 = 1;
attempts = 2;

This is not:

let mut attempts: u32 = 1;
attempts = true; // A Boolean is not a u32.

There is a related distinction worth recognizing when reading Rust code. Repeating let creates a new binding, even if you reuse the same name:

let attempts = 1;
let attempts = attempts + 1;
println!("{attempts}"); // Prints 2.

The second let is called shadowing. It is not a reassignment of the first, immutable binding. Unlike reassignment, a new binding can have a different type. For now, the practical way to distinguish the two is to look for let: attempts = ... tries to change an existing binding, while let attempts = ... declares another one.


See the compiler enforce the distinction

Use the Cargo project from the previous lesson. Replace src/main.rs with the complete connection-counting program above, save it, and run:

cargo check
cargo run

The program should print active: 3; available: 5. The += 1 changes active_connections; it does not change connection_limit.

Now make one temporary edit: remove mut from the active_connections declaration, leaving the += 1 line in place. Run cargo check. The compiler should refuse the reassignment and point you toward making the binding mutable. Restore mut and check again. This is a useful development rhythm: change one declaration, read the compiler’s explanation, and restore the working program before moving on.

For a type error, you can temporarily replace active_connections += 1; with active_connections = true; and run cargo check again. Even with mut, Rust rejects the assignment because the binding has type u32. Restore the increment when you finish.


Where this leaves you

You can now declare an immutable binding with let, opt into reassignment with let mut, and either annotate a type or let Rust infer it from a value and its context. A mutable binding keeps its type; a repeated let creates a new binding rather than changing the old one.

Next, you will work more directly with Rust’s basic data types—including numbers, Booleans, characters, tuples, and arrays—and use them in small programs.

Can't find a good explanation? Sign up and we'll make it for you

Sign up