Rust variable
last modified October 25, 2026
In this article we show how to use variables in Rust.
A variable is a name bound to a value. Rust creates variables with
the let keyword. The binding is immutable by default: once a
value is assigned, it cannot be changed. To create a mutable variable, we add
the mut keyword.
Every variable has a data type, which is known at compile time. A data type is a set of values and the operations allowed on them. In most cases, Rust infers the type from the value or from the way the variable is used; we can also write it explicitly after a colon. The compiler checks that variables are initialized before they are used and that they are used only within their scope.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q. Several examples intentionally
fail to compile; their output shows the real compiler messages.
Defining variables
A variable is defined with let, a name, an optional type, and
an initial value.
fn main() {
let name: &str = "John Doe";
let age: i32 = 34;
println!("{name} is {age} years old");
}
The program defines two variables and prints them.
let name: &str = "John Doe";
The name variable holds a string slice. After the colon, we
specify the data type of the variable. Variable names use the snake_case
convention.
let age: i32 = 34;
The age variable holds a 32-bit signed integer.
println!("{name} is {age} years old");
The variables are used directly in the format string of the
println! macro.
$ cargo run -q John Doe is 34 years old
Type inference
In most cases, the type annotation can be omitted. The compiler infers the type from the value and from later use of the variable.
use std::any::type_name_of_val;
fn main() {
let name = "John Doe";
let age = 34;
let height = 1.78;
let active = true;
let grade = 'A';
println!("{name} is {age} years old");
println!("{}", type_name_of_val(&name));
println!("{}", type_name_of_val(&age));
println!("{}", type_name_of_val(&height));
println!("{}", type_name_of_val(&active));
println!("{}", type_name_of_val(&grade));
let small = 200u8;
let big = 3_000_000_000_i64;
let ratio: f32 = 0.5;
let mut ids = Vec::new();
for i in 1..=3 {
ids.push(i * 7);
}
let first: u16 = ids[0];
println!("{small} {big} {ratio} {ids:?} {first}");
println!("{}", type_name_of_val(&ids));
}
We define variables of several types without annotations and print the inferred types.
let age = 34; let height = 1.78;
An integer literal without other information is an i32; a
floating point literal is an f64. The
type_name_of_val function returns the name of the type of a
value; it is meant for debugging.
let small = 200u8; let big = 3_000_000_000_i64; let ratio: f32 = 0.5;
A different numeric type is chosen with a suffix or with an annotation. Underscores in numeric literals make them easier to read.
let mut ids = Vec::new();
for i in 1..=3 {
ids.push(i * 7);
}
let first: u16 = ids[0];
The type of the vector is not known when it is created. The compiler infers
it from the later use: the first element is assigned to a u16
variable, so the vector is a Vec<u16>.
$ cargo run -q John Doe is 34 years old &str i32 f64 bool char 200 3000000000 0.5 [7, 14, 21] 7 alloc::vec::Vec<u16>
Immutable variables
Variables are immutable by default. Assigning a new value to an immutable variable is a compile-time error.
fn main() {
let age = 34;
println!("age: {age}");
age = 35;
println!("age: {age}");
}
We try to change the value of the age variable.
let age = 34; ... age = 35;
The compiler reports the error E0384 and suggests making the binding
mutable. Immutability by default makes the code easier to reason about: when
we see a let without mut, we know the value does not
change.
$ cargo run -q error[E0384]: cannot assign twice to immutable variable `age` --> src/main.rs:5:5 | 2 | let age = 34; | --- first assignment to `age` ... 5 | age = 35; | ^^^^^^^^ cannot assign twice to immutable variable | help: consider making this binding mutable | 2 | let mut age = 34; | +++ For more information about this error, try `rustc --explain E0384`.
Mutable variables
A mutable variable is created with let mut. Its value can be
changed, but its type stays the same.
fn main() {
let mut name = String::from("John Doe");
let mut age = 34;
println!("{name} is {age} years old");
name = String::from("Roger Roe");
age += 20;
println!("{name} is {age} years old");
name.push_str(" Jr.");
println!("{name}");
}
We assign new values to two mutable variables.
name = String::from("Roger Roe");
age += 20;
A new string is assigned to name; the old one is dropped. The
compound assignment operator += adds to the current value of
age.
name.push_str(" Jr.");
The mut keyword is also needed to modify the value through a
method like push_str. See the
Rust mutability article for more details.
$ cargo run -q John Doe is 34 years old Roger Roe is 54 years old Roger Roe Jr.
Shadowing
A new variable may have the same name as an earlier one. The new variable shadows the previous one: from that point, the name refers to the new variable.
fn main() {
let x = 5;
let x = x * 2;
let x = x + 1;
println!("x: {x}");
let input = " 42 ";
let input = input.trim();
let input: u32 = input.parse().unwrap();
println!("input: {input}");
let y = 10;
{
let y = "inner";
println!("inside the block: {y}");
}
println!("outside the block: {y}");
}
We shadow variables in the same scope and in an inner block.
let x = 5; let x = x * 2; let x = x + 1;
Each let creates a new variable, computed from the previous one.
All the variables are immutable.
let input = " 42 "; let input = input.trim(); let input: u32 = input.parse().unwrap();
Unlike assignment to a mutable variable, shadowing can change the type. This
is common for transformations of a value, where we do not need the previous
form and do not want to invent new names such as input_str.
let y = 10;
{
let y = "inner";
println!("inside the block: {y}");
}
println!("outside the block: {y}");
Shadowing in an inner block ends with the block. After it, the outer
y is visible again with its original value.
$ cargo run -q x: 11 input: 42 inside the block: inner outside the block: 10
Scope
A variable is valid from the point where it is defined to the end of the block in which it is defined. When it goes out of scope, its value is dropped.
fn main() {
let outer = 1;
{
let inner = 2;
println!("{outer} {inner}");
}
println!("{outer} {inner}");
}
We try to use a variable outside of its block.
{
let inner = 2;
println!("{outer} {inner}");
}
println!("{outer} {inner}");
Inside the block, both variables are visible. After the block, the
inner variable no longer exists, and the compiler reports the
error E0425.
$ cargo run -q
error[E0425]: cannot find value `inner` in this scope
--> src/main.rs:9:24
|
9 | println!("{outer} {inner}");
| ^^^^^
|
help: the binding `inner` is available in a different scope in the same function
--> src/main.rs:5:13
|
5 | let inner = 2;
| ^^^^^
For more information about this error, try `rustc --explain E0425`.
Deferred initialization
A variable can be declared first and initialized later. The compiler checks that it is assigned exactly once before it is used, so it does not have to be mutable.
fn main() {
let category;
let temp = 23;
if temp > 25 {
category = "hot";
} else if temp > 15 {
category = "warm";
} else {
category = "cold";
}
println!("{temp} degrees is {category}");
let size = if temp % 2 == 0 { "even" } else { "odd" };
println!("{temp} is {size}");
}
We set the category of a temperature in the branches of an
if statement.
let category;
The variable is declared without a value. Its type is inferred from the assignments.
let size = if temp % 2 == 0 { "even" } else { "odd" };
Often it is simpler to use the fact that if is an expression.
The variable is then initialized directly with the value of the expression.
$ cargo run -q 23 degrees is warm 23 is odd
Uninitialized variables
Using a variable that may not have been initialized is a compile-time error. Rust has no default values for uninitialized variables.
fn main() {
let total: i32;
let ready = false;
if ready {
total = 100;
}
println!("{total}");
}
The total variable is assigned only in one branch.
if ready {
total = 100;
}
println!("{total}");
The compiler does not evaluate the condition; it sees that there is a path
on which total is not assigned. It reports the error E0381 and
points to the missing else arm.
$ cargo run -q
error[E0381]: used binding `total` is possibly-uninitialized
--> src/main.rs:9:16
|
2 | let total: i32;
| ----- binding declared here but left uninitialized
...
5 | if ready {
| ----- if this `if` condition is `false`, `total` is not initialized
6 | total = 100;
7 | }
| - an `else` arm might be missing here, initializing `total`
8 |
9 | println!("{total}");
| ^^^^^ `total` used here but it is possibly-uninitialized
For more information about this error, try `rustc --explain E0381`.
Constants and statics
Constants are defined with the const keyword. They must have a
type annotation, and their value must be computable at compile time. By
convention, their names are in uppercase.
const MAX_USERS: u32 = 100;
const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
static GREETING: &str = "Hello there";
fn main() {
println!("{MAX_USERS}");
println!("{SECONDS_PER_DAY}");
println!("{GREETING}");
const LIMIT: usize = 3;
let data = [0; LIMIT];
println!("{data:?}");
}
We define constants and a static variable.
const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
The expression is evaluated by the compiler. A constant has no fixed memory address; its value is inserted wherever it is used.
static GREETING: &str = "Hello there";
A static item is a single value with a fixed address in memory,
which lives for the whole run of the program.
const LIMIT: usize = 3; let data = [0; LIMIT];
Constants can be defined inside functions as well. Since their values are known at compile time, they can be used as array sizes.
$ cargo run -q 100 86400 Hello there [0, 0, 0]
Destructuring
The left side of let is a pattern. With patterns, we can
define several variables at once from a tuple, an array, or a structure.
struct Point {
x: i32,
y: i32,
}
fn main() {
let (name, age) = ("Jane", 28);
println!("{name} {age}");
let [first, second, third] = [10, 20, 30];
println!("{first} {second} {third}");
let p = Point { x: 3, y: -4 };
let Point { x, y } = p;
println!("x: {x}, y: {y}");
let (a, _, c) = (1, 2, 3);
println!("{a} {c}");
let (mut lo, mut hi) = (1, 9);
(lo, hi) = (hi, lo);
println!("{lo} {hi}");
}
We take values apart into separate variables.
let (name, age) = ("Jane", 28);
let [first, second, third] = [10, 20, 30];
The tuple and the array are destructured into variables.
let Point { x, y } = p;
The fields of the structure are bound to variables with the same names.
let (a, _, c) = (1, 2, 3);
The underscore ignores a value.
(lo, hi) = (hi, lo);
A destructuring assignment changes existing mutable variables. Here it swaps the values of two variables.
$ cargo run -q Jane 28 10 20 30 x: 3, y: -4 1 3 9 1
Unused variables
The compiler warns about variables that are never used. A name that starts with an underscore turns off the warning.
fn main() {
let count = 10;
let _reserved = 20;
let total = 30;
println!("{total}");
}
The count variable is not used.
let count = 10; let _reserved = 20;
The compiler warns about count, but not about
_reserved. Unlike the plain _ pattern, a variable
named _reserved still binds the value, which lives until the end
of the scope. The program compiles and runs despite the warning.
$ cargo run -q warning: unused variable: `count` --> src/main.rs:2:9 | 2 | let count = 10; | ^^^^^ help: if this is intentional, prefix it with an underscore: `_count` | = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 30
Copying and moving
Assigning a variable to another either copies or moves the value. Simple
values such as integers implement the Copy trait and are copied.
Values that own heap data, such as String, are moved.
fn main() {
let a = 5;
let b = a;
println!("{a} {b}");
let s1 = String::from("falcon");
let s2 = s1.clone();
let s3 = s1;
println!("{s2} {s3}");
let r1 = &s3;
let r2 = &s3;
println!("{r1} {r2} {s3}");
}
We assign integers and strings to new variables.
let a = 5; let b = a;
The integer is copied; both variables can be used.
let s2 = s1.clone(); let s3 = s1;
The clone method creates a copy of the string data. The second
line moves the string from s1 to s3; after it,
s1 cannot be used.
let r1 = &s3; let r2 = &s3;
References borrow the value without moving it. There can be any number of shared references at the same time.
$ cargo run -q 5 5 falcon falcon falcon falcon falcon
Using a moved variable
After a move, the original variable cannot be used.
fn main() {
let s1 = String::from("falcon");
let s2 = s1;
println!("{s1} {s2}");
}
We use the s1 variable after its value has been moved to
s2.
let s2 = s1;
println!("{s1} {s2}");
The compiler reports the error E0382 and suggests cloning the value. Only one variable owns the string, so that its memory is freed exactly once. The Rust ownership article explains the rules in detail.
$ cargo run -q
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:16
|
2 | let s1 = String::from("falcon");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1} {s2}");
| ^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
Function parameters
Function parameters are variables too. They are initialized with the arguments of the call and are valid within the function body.
fn build_msg(name: &str, age: u32) -> String {
format!("{name} is {age} years old")
}
fn grow(mut age: u32) -> u32 {
age += 1;
age
}
fn main() {
let name = String::from("John Doe");
let age = 34;
let msg = build_msg(&name, age);
println!("{msg}");
let older = grow(age);
println!("{age} {older}");
}
The program passes variables to two functions.
fn build_msg(name: &str, age: u32) -> String {
format!("{name} is {age} years old")
}
The types of parameters must always be written; they are not inferred. The function borrows the name and receives a copy of the age.
fn grow(mut age: u32) -> u32 {
age += 1;
age
}
A parameter can be declared mut. It changes only the local copy;
the age variable in main keeps its value.
$ cargo run -q John Doe is 34 years old 34 35
Numeric limits
The type of a numeric variable determines the range of values it can hold.
When an arithmetic operation goes beyond the range, the program panics in
debug builds; the checked_, wrapping_, and
saturating_ methods handle overflow explicitly.
fn main() {
let max = i32::MAX;
let min = u8::MIN;
println!("i32: {} to {max}", i32::MIN);
println!("u8: {min} to {}", u8::MAX);
let count: u8 = 255;
println!("{:?}", count.checked_add(1));
println!("{}", count.wrapping_add(1));
println!("{}", count.saturating_add(1));
println!("{}", std::mem::size_of_val(&max));
println!("{}", std::mem::size_of::<char>());
}
We print the limits of two integer types and add one to the maximum value of
u8.
let count: u8 = 255;
println!("{:?}", count.checked_add(1));
println!("{}", count.wrapping_add(1));
println!("{}", count.saturating_add(1));
The checked_add method returns None on overflow,
wrapping_add wraps around to zero, and
saturating_add stays at the maximum.
println!("{}", std::mem::size_of_val(&max));
println!("{}", std::mem::size_of::<char>());
An i32 takes four bytes. A char also takes four
bytes, because it can hold any Unicode scalar value.
$ cargo run -q i32: -2147483648 to 2147483647 u8: 0 to 255 None 0 255 4 4
Source
The Rust Programming Language: Variables and Mutability
The Rust Reference: let statements
In this article we have worked with variables in Rust.