Skip to content

Variables and Data Types

This page explains Rust variables, their mutability, and data types.

Variables

In Rust, a variable is a named binding to a value stored in memory.

Mutability

In Rust, by default, Variables are immutable. That is, once a value is bound to a variable name, it's value can't be changed. You define variables using the let keyword.

rust
let x = 5;

// The following line throws a compilation error, since the variable is immutable
x = 6;

You can, however, have the option to make your variables mutable. You can do so by using mut keyword as shown below:

rust
let mut x = 5;

// The following line works fine now
x = 6;

Constants

Like immutable variables, Constants are values that are bound to a name and are not allowed to change. But there are some differences:

  • Constants are defined with const keyword, instead of the let keyword.
  • Constants aren't just immutable by default - they're always immutable. That is, you're not allowed to use mut with constants.
  • The type of the constant value must be annotated.
  • Constants may be set only to a constant expression, not the result of a value that could only be computed at runtime.
rust
const THREE_HOURS_IN_SECONDS: u32 = 3 * 60 * 60;

The Rust compiler evaluates the above expression at compile time, which lets us choose to write out this value in a way that’s easier to understand and verify, rather than setting this constant to the value 10800.

Naming hardcoded values used throughout your program as constants is useful in conveying the meaning of that value to future maintainers of the code. It also helps to have only one place in your code you would need to change if the hardcoded value needed to be updated in the future.

Static Variables

In Rust, a static variable is a value that has a single, fixed memory location for the entire duration of the program. In other words, it’s a global variable with a static lifetime, meaning it lives for as long as the program runs.

By default, static variables are immutable. To make one mutable, you must use mut keyword but accessing or modifying it requires an unsafe block. This is because mutable statics can cause data races if used concurrently.

Static variables should be used for truly global data that never changes (Ex: version strings, configuration constants).

Shadowing

In Rust, shadowing is when you declare a new variable with the same name as a previous one, effectively “shadowing” the original variable. The new variable hides the old one within its scope, even though both exist in memory at different times.

rust
fn main() {
    let x = 5;
    let x = x + 1; // shadowing: creates a new `x`, not mutating the old one
    {
        let x = x * 2; // shadowing: creates a new `x`, not mutating the old one
        println!("The value of x in the inner scope is: {x}"); // Prints: 12
    }
    println!("The value of x is: {x}"); // Prints: 6
}

A few points to note:

  • Shadowing is not the same as mutation.
    • When you shadow, you create a new variable; When you mutate, you change an existing one.
  • Type can change when shadowing.
    • Since it’s a new variable, you can even change its type.
rust
let spaces = "   "; // `spaces` is a string type
let spaces = spaces.len(); // Now `spaces` is a number type

Shadowing is commonly used to perform a few transformations on a value without needing a new name, but still have the variable be immutable after those transformations.

Naming Conventions

  • Use snake case for variable and function names (lower cased).
  • Names should be descriptive but concise.
  • Avoid abbreviations unless they're widely understood (Ex: id, url, api).
  • Avoid using keywords for variable or function names.
  • Use upper cased names for constants and static variables.
rust
let user_name = "Alice";
let account_balance = 250.75;
const MAX_POINTS: u32 = 100;
static APP_VERSION: &str = "1.0.0";

Data Types

Every Value in Rust is of a certain data type. Rust is a statically typed language, which means all variable types must be known at compile time. The compiler can usually infer the type based on the value assigned. In cases when the compiler doesn’t have enough information to guess the type, you must add a type annotation, like this:

rust
let guess: i32 = "42".parse().unwrap();

Rust’s data types are grouped into two subsets:

  • Scalar types – represent a single value
  • Compound types – group multiple values together

Scalar Types

Rust has four primary scalar types: integers, floating-point numbers, Booleans and characters.

Integers

An integer is a number without a fractional component.

Integers can be signed (types start with i) or unsigned (types start with u). Unsigned integers can only store non-negative numbers, whereas signed integers can store both positive and negative numbers. The first bit of a signed integer is used as a sign bit - 0 for positive, 1 for negative.

The following variants can be used to declare the type of an integer value:

TypeSize (bits)Range (approx.)
i8 / u88−128 → 127 / 0 → 255
i16 / u1616−32,768 → 32,767 / 0 → 65,535
i32 / u3232−2.1B → 2.1B / 0 → 4.2B
i64 / u6464very large range
i128 / u128128extremely large range
isize / usizedepends on system architecture (32-bit or 64-bit)

You can write integer literals in any of the forms shown below:

Number LiteralsExample
Decimal98_222
Hexadecimal0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

A few points to note:

  • Some numeric literals can represent multiple integer types, can specify their type explicitly using a type suffix. For example, 57u8 indicates an unsigned 8-bit integer.
  • Number literals can also use _ as a visual separator to make the number easier to read, such as 1_000, which will have the same value as if you had specified 1000.

NOTE

In Rust, integer types default to i32.

Integer Overflow

If a value goes outside this range, integer overflow occurs. For example, a u8 can store values from 0 to 255 and if the value goes beyond 255, an integer overflow occurs.

In debug mode, Rust checks for overflow and panics at runtime if it happens. panics are discussed in detail later.

In release mode (--release), Rust does not panic. Instead, it uses two’s complement wrapping. This means values “wrap around” to the start of the range. For example, 256 becomes 0, 257 becomes 1, and so on.

To handle overflow safely, you can use these standard library methods on numeric types:

  • wrapping_* — always wraps around
  • checked_* — returns None on overflow
  • overflowing_* — returns the value and a flag
  • saturating_* — clamps to the min or max value

Example:

rust
let a: u8 = 250;

// overflowing_* → returns (value, overflowed)
let (val, overflowed) = a.overflowing_add(10); // 250 + 10 = 4 (overflows and wraps to 0–255 range)
println!("overflowing_add: value={val}, overflowed={overflowed}"); // prints overflowing_add: value=4, overflowed=true

Floating-Point Types

Rust also has two primitive types for floating-point numbers, which are numbers with decimal points. Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision.

NOTE

In Rust, all floating-point types are signed.

rust
fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}
Numeric Operations

Rust supports the basic mathematical operations you’d expect for all the number types: addition, subtraction, multiplication, division, and remainder. Integer division truncates toward zero to the nearest integer.

rust
fn main() {
    // division
    let quotient = 56.7 / 32.2; // 1.7608695652173911
    let truncated = -5 / 3; // -1
    let error = 5.0 / 3; // Compilation Error: Cannot divide float by integer
}

Boolean Type

Boolean type in Rust has two possible values: true and false. Booleans are one byte in size.

rust
fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Character Type

Rust’s char type is the language’s most primitive alphabetic type. Here are some examples of declaring char values:

rust
fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes. Rust’s char type is four bytes in size and represents a Unicode scalar value, which means it can represent a lot more than just ASCII. Unicode characters are discussed in detail later.

Compound Types

Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

Tuple Type

A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.

We create a tuple by writing a comma-separated list of values inside parentheses. Each position in the tuple has a type, and the types of the different values in the tuple don’t have to be the same. We’ve added optional type annotations in this example:

rust
fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

The tuple without any values has a special name, unit. This value and its corresponding type are both written () and represent an empty value or an empty return type.

Accessing Tuple elements

You can access Tuple elements using their respective indices (index starts with 0) and dot notation, as shown below:

rust
fn main() {
    let tup = (500, 6.4, 1);

    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

You can also use Tuple destructuring to get the individual values out of a tuple, as shown below:

rust
fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;
    println!("The value of y is: {y}");
}

Array Type

Array type is similar to Tuple type, but every element of an array must have the same type. Also, unlike arrays in some other languages, arrays in Rust have a fixed length.

We write the values in an array as a comma-separated list inside square brackets:

rust
fn main() {
    let a = [1, 2, 3, 4, 5];
}

Arrays are useful when you want your data allocated on the stack (LIFO), rather than heap. Also, Array isn't as flexible as a vector type. A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size because its contents live on the heap. Stack, heap and vectors are discussed in detail later.

If you’re unsure whether to use an array or a vector, chances are you should use a vector. However, arrays are more useful when you know the number of elements will not need to change. For example, storing all the 12 month names in an array.

Array's type is declared using square brackets with the type of each element, a semicolon, and then the number of elements in the array, like so:

rust
let a: [i32; 5] = [1, 2, 3, 4, 5];

You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets, as shown here:

rust
let a = [3; 5];
// Same as let a = [3, 3, 3, 3, 3];
Accessing Array Elements

You can access elements of an array using indexing, like this:

rust
fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

If you try to access an element outside the index bounds, you'll get a compilation error. In case the index value is derived at runtime (such as when it's passed from user input), the code panics with index out of bounds.