Site

Variables — Rust Bindings and Types

Tutorial 3.0  •  Rust / Learn

3.0 What This Teaches

This tutorial covers how Rust handles variables and the primitive types you will use in almost every program:

3.1 Bindings, Not Variables

Rust uses the word binding intentionally. let x = 5 does not declare a variable in the traditional sense - it binds the name x to the value 5. The distinction matters because bindings are immutable by default. Trying to change x after binding it is a compile error, not a runtime error.
let x = 5;
x = 10;  // compile error: cannot assign twice to immutable variable
Immutability by default is a deliberate design choice. It makes it easier to reason about where values change, which becomes critical when you add concurrency later.

3.2 Mutable Bindings

When you do need to change a value, add mut:
let mut count = 0;
count += 1;
println!("count = {count}");  // prints: count = 1
mut is explicit and visible at the declaration site. Any reader of the code immediately knows count will change somewhere below.

3.3 Type Inference

Rust infers the type of a binding from the value you assign. You rarely need to write the type explicitly:
let x = 5;        // inferred as i32
let ratio = 3.14; // inferred as f64
let flag = true;  // inferred as bool
When the inferred type is not what you want, annotate it explicitly:
let ratio: f64 = 3.14;
let small: i8  = 127;
The annotation sits between the name and the =, separated by :.

3.4 Integer Types

Rust provides signed and unsigned integers at several sizes:
TypeSizeRange
i88-bit-128 to 127
i1616-bit-32,768 to 32,767
i3232-bit-2,147,483,648 to 2,147,483,647
i6464-bit±9.2 × 10¹&sup8;
i128128-bitvery large
u88-bit0 to 255
u1616-bit0 to 65,535
u3232-bit0 to 4,294,967,295
u6464-bit0 to 1.8 × 10¹&sup9;
usizepointer-sizedplatform-dependent
The default integer type when Rust infers is i32. Use usize for array indices and collection lengths - the standard library expects it. You can use underscores in numeric literals for readability: 1_000_000 is the same as 1000000.

3.5 Floating-Point Types

Rust has two floating-point types:
TypeSizePrecision
f3232-bit~7 decimal digits
f6464-bit~15 decimal digits
The default inferred float type is f64. Prefer f64 unless you have a specific reason to use f32 (memory pressure, hardware requirements).

3.6 Boolean

bool holds either true or false. It is the required type for if conditions - Rust does not coerce integers to booleans the way C does.
let is_ready: bool = true;

if is_ready {
    println!("ready");
}

3.7 Character

char holds a single Unicode scalar value and occupies 4 bytes. That is broader than ASCII - a char can hold any character from any human language.
let letter: char = 'R';
let emoji: char  = '🦀';  // the Rust mascot
Use single quotes for char literals. Double quotes are for string literals (&str).

3.8 Shadowing

Shadowing rebinds a name to a new value in the same scope. The old binding is hidden but not mutated:
let x = 5;
let x = x * 2;  // new binding; shadows the first x
println!("x = {x}");  // prints: x = 10
Shadowing differs from mut in two ways:

3.9 Example - All Together

// Variables - demonstrates bindings, mutability, primitive types, and shadowing in Rust.
fn main() {
    let x = 5;
    println!("x = {x}");

    let mut count = 0;
    count += 1;
    println!("count = {count}");

    let ratio: f64 = 3.14;
    println!("ratio = {ratio}");

    let is_ready: bool = true;
    println!("is_ready = {is_ready}");

    let letter: char = 'R';
    println!("letter = {letter}");

    let x = x * 2;
    println!("x after shadowing = {x}");

    let big: i64  = 1_000_000;
    let small: i8 = 127;
    println!("big = {big}, small = {small}");

    let index: usize = 42;
    println!("index = {index}");
}
Expected output:
x = 5
count = 1
ratio = 3.14
is_ready = true
letter = R
x after shadowing = 10
big = 1000000, small = 127
index = 42

3.10 Exercise

Exercise
  • Declare an immutable binding for your age as u8. Print it.
  • Declare a mutable binding for a temperature as f32. Change it and print both values.
  • Shadow a binding: first bind a name to 3 (an integer), then shadow it with the string "three". Print the shadowed value. Notice that shadowing allows a type change.

3.11 Common Mistakes

Trying to mutate an immutable binding

let x = 5;
x = 10;  // error: cannot assign twice to immutable variable `x`
Fix: add mut to the declaration.

Mixing integer types without casting

let a: i32 = 10;
let b: i64 = 20;
let c = a + b;  // error: mismatched types
Rust does not silently widen integer types. Fix: cast explicitly with a as i64 + b.

Confusing char and &str literal syntax

let c = "R";  // this is &str, not char
let c = 'R';  // this is char
Single quotes for char, double quotes for string slices.

Assuming default integer is i64

The default inferred integer type is i32, not i64. Values that overflow i32::MAX (2,147,483,647) cause a panic in debug mode. Annotate explicitly when you need a larger range.

3.12 Key Terms

TermMeaning
bindingA name bound to a value via let
immutableCannot be changed after binding (the default)
mutKeyword that makes a binding mutable
type inferenceCompiler deduces the type from the assigned value
shadowingRebinding a name in the same scope with a new let
i32Default signed 32-bit integer
f64Default 64-bit floating-point number
usizePointer-sized unsigned integer; used for indices and lengths
charA single Unicode scalar value, 4 bytes