Rust tuples
last modified November 13, 2026
In this article we work with tuples in Rust.
A tuple is a fixed-size, ordered group of values. Unlike an
array, the values of a tuple can have different types. The type of a tuple
is written as the list of its element types in parentheses; for example,
(&str, i32, bool). The number of elements is part of the
type, so a tuple can neither grow nor shrink.
Tuples are useful for short-lived groupings of values: returning several
values from a function, iterating over pairs, or using a compound key in a
map. When the values deserve names, a struct is usually the better choice.
The examples were tested with Rust 1.97 and the 2024 edition. They are run
as Cargo projects with cargo run -q.
Creating tuples
A tuple is created by writing comma-separated values in parentheses.
fn main() {
let user = ("John Doe", 34, true);
let point: (f64, f64) = (2.5, -1.0);
let nested = (1, ("a", 'b'), [3, 4]);
println!("{user:?}");
println!("{point:?}");
println!("{nested:?}");
println!("{nested:#?}");
}
We create three tuples and print them with the debug formatter.
let user = ("John Doe", 34, true);
The type of user is inferred as
(&str, i32, bool).
let point: (f64, f64) = (2.5, -1.0);
The type can also be written explicitly.
let nested = (1, ("a", 'b'), [3, 4]);
Tuples can contain other tuples, arrays, and any other values.
println!("{nested:#?}");
Tuples implement Debug when all their elements do. The
{:#?} form prints each element on its own line.
$ cargo run -q
("John Doe", 34, true)
(2.5, -1.0)
(1, ("a", 'b'), [3, 4])
(
1,
(
"a",
'b',
),
[
3,
4,
],
)
Accessing elements
Tuple elements are accessed with a dot followed by the index of the element. Indexes start from zero.
fn main() {
let user = ("John Doe", 34, "gardener");
println!("name: {}", user.0);
println!("age: {}", user.1);
println!("occupation: {}", user.2);
let nested = ((1, 2), (3, 4));
println!("{}", nested.1.0);
let mut pos = (0, 0);
pos.0 += 5;
pos.1 -= 2;
println!("{pos:?}");
}
The example reads elements of a tuple and modifies a mutable one.
println!("name: {}", user.0);
The user.0 expression is the first element. The index must be
a literal number; it cannot be a variable, because each element may have
a different type and the compiler must know which one is meant.
let nested = ((1, 2), (3, 4));
println!("{}", nested.1.0);
Indexes can be chained to reach into nested tuples.
let mut pos = (0, 0); pos.0 += 5; pos.1 -= 2;
The elements of a mutable tuple can be changed one by one.
$ cargo run -q name: John Doe age: 34 occupation: gardener 3 (5, -2)
Indexes are checked at compile time
Since the length of a tuple is part of its type, accessing an element that does not exist is a compile error, not a runtime panic.
fn main() {
let user = ("John Doe", 34);
println!("{}", user.2);
}
The tuple has two elements, but we ask for the third.
println!("{}", user.2);
The compiler reports error E0609 and lists the available fields. Compare this with a vector, where an index out of bounds panics at runtime.
$ cargo run -q
error[E0609]: no field `2` on type `(&str, {integer})`
--> src/main.rs:3:25
|
3 | println!("{}", user.2);
| ^ unknown field
|
= note: available fields are: `0`, `1`
For more information about this error, try `rustc --explain E0609`.
Destructuring
A tuple pattern on the left side of let splits a tuple into
separate variables. This is called destructuring.
fn main() {
let user = ("Roger Roe", 41, "teacher", "Bratislava");
let (name, age, occupation, city) = user;
println!("{name} is {age}, {occupation} from {city}");
let (name, _, _, city) = user;
println!("{name} lives in {city}");
let (first, ..) = user;
let (.., last) = user;
println!("{first} / {last}");
let ((x1, y1), (x2, y2)) = ((1, 2), (4, 6));
println!("dx={} dy={}", x2 - x1, y2 - y1);
}
We unpack a tuple of four elements in several ways.
let (name, age, occupation, city) = user;
The pattern must have the same number of elements as the tuple. Each variable gets the value at its position.
let (name, _, _, city) = user;
The _ wildcard ignores a value.
let (first, ..) = user; let (.., last) = user;
The .. rest pattern skips any number of elements. It can be
used once in a pattern.
let ((x1, y1), (x2, y2)) = ((1, 2), (4, 6));
Patterns can be nested to destructure nested tuples in one step.
$ cargo run -q Roger Roe is 41, teacher from Bratislava Roger Roe lives in Bratislava Roger Roe / Bratislava dx=3 dy=4
Destructuring assignment
A tuple pattern can also appear on the left side of an ordinary assignment to existing variables.
fn main() {
let mut a = 1;
let mut b = 2;
(a, b) = (b, a);
println!("a={a} b={b}");
let (mut x, mut y) = (0, 1);
for _ in 0..10 {
(x, y) = (y, x + y);
}
println!("fib(10) = {x}");
let mut count;
let total;
(count, total) = (3, 30);
count += 1;
println!("{count} {total}");
}
The example swaps two values and computes a Fibonacci number.
(a, b) = (b, a);
The right side builds a tuple from the current values first, so the two variables are swapped without a temporary variable.
for _ in 0..10 {
(x, y) = (y, x + y);
}
Both new values are computed from the old ones. With two separate
assignments, the second would already see the changed x.
let mut count; let total; (count, total) = (3, 30);
Destructuring assignment can also initialize variables declared earlier.
The immutable total is assigned exactly once.
$ cargo run -q a=2 b=1 fib(10) = 55 4 30
Returning multiple values
A function returns a single value, but that value can be a tuple. This is the usual way to return several results.
fn min_max(vals: &[i32]) -> Option<(i32, i32)> {
let first = *vals.first()?;
Some(vals.iter().fold((first, first), |(lo, hi), &v| (lo.min(v), hi.max(v))))
}
fn div_rem(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
fn main() {
let vals = vec![4, -2, 9, 0, 7];
if let Some((lo, hi)) = min_max(&vals) {
println!("min: {lo}, max: {hi}");
}
println!("{:?}", min_max(&vals[..0]));
let (q, r) = div_rem(17, 5);
println!("17 = 5 * {q} + {r}");
let text = "key=value";
if let Some((k, v)) = text.split_once('=') {
println!("{k} -> {v}");
}
}
The min_max function finds the smallest and the largest
value in one pass.
fn min_max(vals: &[i32]) -> Option<(i32, i32)> {
let first = *vals.first()?;
Some(vals.iter().fold((first, first), |(lo, hi), &v| (lo.min(v), hi.max(v))))
}
An empty slice has no minimum, so the pair is wrapped in an
Option. The fold accumulator is itself a tuple,
destructured in the closure parameters.
if let Some((lo, hi)) = min_max(&vals) {
A tuple pattern can be nested inside other patterns, here inside
Some.
if let Some((k, v)) = text.split_once('=') {
The standard library uses tuples in the same way. The
split_once method returns the parts before and after the
separator as Option<(&str, &str)>.
$ cargo run -q min: -2, max: 9 None 17 = 5 * 3 + 2 key -> value
Tuple patterns in parameters
Function and closure parameters are patterns too, so a tuple can be destructured directly in the parameter list.
fn distance((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
fn describe(&(name, age): &(&str, u32)) -> String {
format!("{name} ({age})")
}
fn main() {
println!("{}", distance((0.0, 0.0), (3.0, 4.0)));
let people = [("Anna", 31), ("Peter", 45)];
let labels: Vec<String> = people.iter().map(describe).collect();
println!("{labels:?}");
}
The distance function computes the distance between two
points.
fn distance((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 {
Each parameter is a tuple pattern followed by the tuple type. The caller passes two tuples; the body works with four named numbers.
fn describe(&(name, age): &(&str, u32)) -> String {
The &(name, age) pattern matches a reference to a tuple
and copies the elements out of it. This works because both elements are
Copy. The function fits iter().map, which passes
references to the elements.
$ cargo run -q 5 ["Anna (31)", "Peter (45)"]
The unit type
The empty tuple () is called the unit type. It has
a single value, also written (), and carries no information.
use std::any::type_name_of_val;
use std::mem::size_of;
fn greet(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let f: fn(&str) -> () = greet;
f("Lucia");
println!("{:?}", ());
println!("size of (): {}", size_of::<()>());
let single = (5,);
println!("{single:?} {}", type_name_of_val(&single));
let results: Vec<Result<(), String>> = vec![Ok(()), Err("disk full".to_string())];
for r in &results {
match r {
Ok(()) => println!("saved"),
Err(e) => println!("error: {e}"),
}
}
}
Functions without a return type return the unit value.
let f: fn(&str) -> () = greet;
The greet function declares no return type; its full type is
fn(&str) -> (). Writing -> () by hand
is allowed but unnecessary.
println!("size of (): {}", size_of::<()>());
The unit type is a zero-sized type. It takes no memory.
let single = (5,);
A tuple with one element needs a trailing comma. Without it,
(5) is just the number five in parentheses, and the compiler
warns about unnecessary parentheses.
let results: Vec<Result<(), String>> = vec![Ok(()), Err("disk full".to_string())];
Result<(), E> is the type of an operation that either
succeeds without producing a value or fails with an error. Many standard
functions, such as std::fs::write, return
io::Result<()>.
$ cargo run -q Hello, Lucia! () size of (): 0 (5,) (i32,) saved error: disk full
Comparing and sorting tuples
Tuples implement PartialEq, Eq,
PartialOrd, Ord, and Hash when
their elements do. They are compared lexicographically: element by
element, from left to right.
fn main() {
println!("{}", (1, 2, 3) < (1, 3, 0));
println!("{}", ("b", 1) > ("a", 9));
println!("{}", (2, "x") == (2, "y"));
let mut scores = vec![("Peter", 82), ("Anna", 95), ("Lucia", 82), ("Bob", 70)];
scores.sort();
println!("{scores:?}");
scores.sort_by_key(|&(name, score)| (std::cmp::Reverse(score), name));
println!("{scores:?}");
let best = scores.iter().max_by_key(|&&(_, s)| s).unwrap();
println!("best: {best:?}");
}
The example compares tuples and sorts a vector of pairs.
println!("{}", (1, 2, 3) < (1, 3, 0));
The first elements are equal, so the second ones decide: 2 is less than 3. The third elements are not compared at all.
scores.sort();
Sorting pairs orders them by the first element, the name. The score only matters for equal names.
scores.sort_by_key(|&(name, score)| (std::cmp::Reverse(score), name));
Returning a tuple from a key function is a simple way to sort by several criteria. The scores are sorted in descending order, and equal scores are ordered by name.
$ cargo run -q
true
true
false
[("Anna", 95), ("Bob", 70), ("Lucia", 82), ("Peter", 82)]
[("Anna", 95), ("Lucia", 82), ("Peter", 82), ("Bob", 70)]
best: ("Anna", 95)
Tuples in iterators
Several iterator adapters produce tuples, and collections of tuples can be turned into other collections.
use std::collections::HashMap;
fn main() {
let names = ["Anna", "Peter", "Lucia"];
let ages = [31, 45, 27];
for (i, name) in names.iter().enumerate() {
println!("{i}: {name}");
}
let people: Vec<(&str, i32)> = names.into_iter().zip(ages).collect();
println!("{people:?}");
for &(name, age) in &people {
if age > 30 {
println!("{name} is over 30");
}
}
let lookup: HashMap<&str, i32> = people.iter().copied().collect();
println!("Peter: {}", lookup["Peter"]);
let (ns, as_): (Vec<&str>, Vec<i32>) = people.into_iter().unzip();
println!("{ns:?} {as_:?}");
}
We combine two arrays into a vector of pairs.
for (i, name) in names.iter().enumerate() {
The enumerate adapter yields (index, element)
tuples. The for loop destructures them.
let people: Vec<(&str, i32)> = names.into_iter().zip(ages).collect();
The zip adapter walks two iterators in step and yields pairs.
for &(name, age) in &people {
Iterating over &people yields references to tuples; the
&(name, age) pattern copies out the elements.
let lookup: HashMap<&str, i32> = people.iter().copied().collect();
An iterator of key-value pairs can be collected into a
HashMap.
let (ns, as_): (Vec<&str>, Vec<i32>) = people.into_iter().unzip();
The unzip method is the opposite of zip: it
splits pairs into two collections.
$ cargo run -q
0: Anna
1: Peter
2: Lucia
[("Anna", 31), ("Peter", 45), ("Lucia", 27)]
Anna is over 30
Peter is over 30
Peter: 45
["Anna", "Peter", "Lucia"] [31, 45, 27]
Tuples as map keys
Because tuples implement Hash and Eq, they work
as compound keys in a HashMap.
use std::collections::HashMap;
fn main() {
let mut board: HashMap<(i32, i32), char> = HashMap::new();
board.insert((0, 0), 'X');
board.insert((1, 1), 'O');
board.insert((2, 2), 'X');
for row in 0..3 {
let line: String = (0..3)
.map(|col| *board.get(&(row, col)).unwrap_or(&'.'))
.collect();
println!("{line}");
}
let mut sales: HashMap<(&str, u32), f64> = HashMap::new();
let data = [("north", 2025, 120.5), ("south", 2025, 80.0), ("north", 2025, 30.0), ("north", 2026, 55.0)];
for (region, year, amount) in data {
*sales.entry((region, year)).or_default() += amount;
}
let mut keys: Vec<_> = sales.keys().collect();
keys.sort();
for k in keys {
println!("{k:?}: {}", sales[k]);
}
}
The first map stores the marks of a tic-tac-toe board under
(row, column) keys.
let mut board: HashMap<(i32, i32), char> = HashMap::new(); board.insert((0, 0), 'X');
Only the occupied squares are stored. Empty squares are printed as dots.
*sales.entry((region, year)).or_default() += amount;
The second map sums sales per region and year. A loop over an array of three-element tuples destructures each record.
let mut keys: Vec<_> = sales.keys().collect(); keys.sort();
A HashMap iterates in an unspecified order. Sorting the keys
gives a stable output; the tuple keys sort by region and then by year.
$ cargo run -q
X..
.O.
..X
("north", 2025): 150.5
("north", 2026): 55
("south", 2025): 80
Matching on tuples
A tuple of values can be matched against several patterns at once.
fn fizzbuzz(n: u32) -> String {
match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
_ => n.to_string(),
}
}
fn quadrant(p: (i32, i32)) -> &'static str {
match p {
(0, 0) => "origin",
(_, 0) | (0, _) => "on an axis",
(x, y) if x > 0 && y > 0 => "first",
(x, y) if x < 0 && y > 0 => "second",
(_, y) if y < 0 => "third or fourth",
_ => unreachable!(),
}
}
fn main() {
let words: Vec<String> = (1..=15).map(fizzbuzz).collect();
println!("{}", words.join(" "));
for p in [(0, 0), (3, 0), (2, 5), (-1, 4), (-2, -2)] {
println!("{p:?} {}", quadrant(p));
}
}
The classic FizzBuzz puzzle becomes a single match.
match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
_ => n.to_string(),
}
We build a tuple of both remainders and match it. The arms are checked
from top to bottom, so (0, 0) must come first.
(_, 0) | (0, _) => "on an axis", (x, y) if x > 0 && y > 0 => "first",
Tuple patterns combine with alternatives (|), bindings, and
if guards. Pattern matching is covered in detail in
Rust match expression.
$ cargo run -q 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz (0, 0) origin (3, 0) on an axis (2, 5) first (-1, 4) second (-2, -2) third or fourth
Tuple structs
A tuple struct is a named type with unnamed fields. It has the shape of a tuple, but it is a distinct type.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point(i32, i32);
struct Meters(f64);
struct Feet(f64);
fn to_meters(f: Feet) -> Meters {
Meters(f.0 * 0.3048)
}
fn main() {
let p = Point(3, 4);
let Point(x, y) = p;
println!("{p:?} x={x} y={y} first={}", p.0);
let t: (i32, i32) = (3, 4);
println!("{}", t == (p.0, p.1));
let height = Feet(6.0);
let m = to_meters(height);
println!("{:.3} m", m.0);
}
We define a point and two wrapper types for units of length.
struct Point(i32, i32);
The fields are accessed with .0 and .1, as in a
tuple, and the struct is destructured with Point(x, y).
struct Meters(f64); struct Feet(f64);
A tuple struct with a single field is called a newtype. Both
types wrap an f64, but the compiler treats them as
unrelated, which prevents mixing up the units.
$ cargo run -q Point(3, 4) x=3 y=4 first=3 true 1.829 m
Newtypes prevent mistakes
The following program passes meters to a function that expects feet.
struct Meters(f64);
struct Feet(f64);
fn to_meters(f: Feet) -> Meters {
Meters(f.0 * 0.3048)
}
fn main() {
let m = Meters(10.0);
let converted = to_meters(m);
println!("{}", converted.0);
}
With plain f64 values, the program would compile and
silently compute a wrong result.
let m = Meters(10.0); let converted = to_meters(m);
The newtypes turn the mistake into a compile error. A plain tuple
(f64,) would not help here: two tuples with the same element
types are the same type.
$ cargo run -q
error[E0308]: mismatched types
--> src/main.rs:11:31
|
11 | let converted = to_meters(m);
| --------- ^ expected `Feet`, found `Meters`
| |
| arguments to this function are incorrect
|
note: function defined here
--> src/main.rs:5:4
|
5 | fn to_meters(f: Feet) -> Meters {
| ^^^^^^^^^ -------
For more information about this error, try `rustc --explain E0308`.
Copying and moving tuples
A tuple is Copy when all its elements are. Otherwise,
assigning it moves it.
fn main() {
let a = (1, 2.5, 'x');
let b = a;
println!("{a:?} {b:?}");
let c = (String::from("owned"), 5);
let d = c.clone();
let (s, n) = c;
println!("{s} {n} {d:?}");
let pair = (String::from("left"), String::from("right"));
let (ref l, _) = pair;
println!("{l} {pair:?}");
let (left, right) = pair;
println!("{left} {right}");
}
We compare a tuple of numbers with a tuple containing a
String.
let a = (1, 2.5, 'x'); let b = a;
All three elements are Copy, so a is copied and
stays usable.
let d = c.clone(); let (s, n) = c;
The String element makes the tuple a move type. We clone it
first, then destructure the original, which moves the string into
s.
let (ref l, _) = pair;
println!("{l} {pair:?}");
The ref keyword binds a reference instead of moving the
value, and _ does not bind anything, so pair
stays whole. The final let (left, right) = pair; moves both
strings out.
$ cargo run -q
(1, 2.5, 'x') (1, 2.5, 'x')
owned 5 ("owned", 5)
left ("left", "right")
left right
Memory layout
A tuple stores its elements inline, like a struct. The compiler may reorder them and add padding to satisfy alignment.
use std::mem::size_of;
fn main() {
println!("(u8, u8): {}", size_of::<(u8, u8)>());
println!("(u8, u32): {}", size_of::<(u8, u32)>());
println!("(u8, u32, u8): {}", size_of::<(u8, u32, u8)>());
println!("(u64, u8): {}", size_of::<(u64, u8)>());
println!("(i32, f64, bool): {}", size_of::<(i32, f64, bool)>());
println!("[(u8, u32); 10]: {}", size_of::<[(u8, u32); 10]>());
}
We print the sizes of several tuple types.
println!("(u8, u32, u8): {}", size_of::<(u8, u32, u8)>());
The elements take six bytes, but a u32 must start at a
multiple of four. The compiler places the two bytes next to each other
and the tuple takes eight bytes instead of twelve. The default layout is
not guaranteed, so code must not depend on the order of fields in memory.
println!("(u64, u8): {}", size_of::<(u64, u8)>());
The size of a type is a multiple of its alignment, so that elements of an array of such tuples stay aligned. Nine bytes of data become sixteen.
$ cargo run -q (u8, u8): 2 (u8, u32): 8 (u8, u32, u8): 8 (u64, u8): 16 (i32, f64, bool): 16 [(u8, u32); 10]: 80
The twelve-element limit
Tuples can have any number of elements, but the standard library
implements traits such as Debug, PartialEq, and
Default only for tuples of up to twelve elements.
fn main() {
let t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
println!("{t:?}");
}
A tuple of thirteen numbers is a valid value, but it cannot be printed.
println!("{t:?}");
The compiler reports that Debug is not implemented. Large
tuples are hard to read anyway; a struct with named fields is the right
tool for that much data.
$ cargo run -q
error[E0277]: `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` doesn't implement `std::fmt::Debug`
--> src/main.rs:3:15
|
3 | println!("{t:?}");
| ^^^^^ `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`
|
= help: the trait `std::fmt::Debug` is not implemented for `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})`
= help: the following other types implement trait `std::fmt::Debug`:
()
(A, Z, Y, X, W, V, U, T)
(B, A, Z, Y, X, W, V, U, T)
(C, B, A, Z, Y, X, W, V, U, T)
(D, C, B, A, Z, Y, X, W, V, U, T)
(E, D, C, B, A, Z, Y, X, W, V, U, T)
(T,)
(U, T)
and 5 others
For more information about this error, try `rustc --explain E0277`.
For more information about this error, try `rustc --explain E0277`.
Source
In this article we have worked with tuples in Rust. Named groupings of values are covered in Rust structs, and patterns in Rust pattern matching.
Author
List all Rust tutorials.