Tuples in Rust
A tuple is a fixed-size, ordered collection of values that can each have a different type. Tuples are useful when you want to group together a small number of values without defining a named struct.
Creating Tuples
You create a tuple by writing a comma-separated list of values inside parentheses. Type annotations are optional when the compiler can infer them.
fn main() {
// Explicit type annotation
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Inferred types
let point = (3.0, -1.5);
let mixed = (42, true, 'z', "hello");
println!("{:?}", tup);
println!("{:?}", point);
println!("{:?}", mixed);
}Accessing Elements by Index
You can access individual tuple elements using dot notation followed by a zero-based index.
fn main() {
let tup = (500, 6.4, true);
let five_hundred = tup.0;
let six_point_four = tup.1;
let flag = tup.2;
println!("{}", five_hundred);
println!("{}", six_point_four);
println!("{}", flag);
}Destructuring Tuples
Destructuring lets you bind each element of a tuple to its own variable in a single let statement. This is often cleaner than accessing elements by index.
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("x = {}", x);
println!("y = {}", y);
println!("z = {}", z);
}Returning Multiple Values from Functions
Rust functions can only return a single value, but that value can be a tuple. This is the idiomatic way to return multiple results without defining a new type.
fn min_max(data: &[i32]) -> (i32, i32) {
let mut min = data[0];
let mut max = data[0];
for &val in data.iter() {
if val < min { min = val; }
if val > max { max = val; }
}
(min, max)
}
fn main() {
let numbers = [3, 1, 4, 1, 5, 9, 2, 6];
let (lo, hi) = min_max(&numbers);
println!("min = {}, max = {}", lo, hi);
}The Unit Tuple `()`
The empty tuple () is called the unit type. It represents the absence of a meaningful value — similar to void in other languages. Functions that do not explicitly return anything actually return ().
fn greet(name: &str) -> () {
println!("Hello, {}!", name);
}
fn main() {
let result = greet("Rustacean"); // result has type ()
println!("{:?}", result); // prints ()
}Nested Tuples
Tuples can be nested inside other tuples. Each nesting level is its own type.
fn main() {
let nested = ((1, 2), (3, 4));
// Access with chained dot notation
println!("nested.0.0 = {}", nested.0.0);
println!("nested.0.1 = {}", nested.0.1);
println!("nested.1.0 = {}", nested.1.0);
// Destructure nested tuples
let ((a, b), (c, d)) = nested;
println!("a={} b={} c={} d={}", a, b, c, d);
}Pattern Matching on Tuples
Tuples work naturally in match expressions. You can match on specific values, bind variables, or use _ as a wildcard.
fn classify_point(point: (i32, i32)) -> &'static str {
match point {
(0, 0) => "origin",
(x, 0) if x > 0 => "positive x-axis",
(0, y) if y > 0 => "positive y-axis",
(x, y) if x > 0 && y > 0 => "first quadrant",
_ => "somewhere else",
}
}
fn main() {
println!("{}", classify_point((0, 0)));
println!("{}", classify_point((5, 0)));
println!("{}", classify_point((3, 7)));
println!("{}", classify_point((-2, 4)));
}Printing Tuples
Tuples do not implement the Display trait, so you cannot use {} to print them. Use the Debug format
{:?} (or the pretty-print {:#?}) instead.
fn main() {
let t = (1, "hello", 3.14);
// println!("{}", t); // compile error: doesn't implement Display
println!("{:?}", t); // debug format — works
println!("{:#?}", t); // pretty-print debug
}The Swap Pattern
Tuples make swapping two values clean and allocation-free — no temporary variable required.
fn main() {
let mut a = 10;
let mut b = 20;
println!("before: a={}, b={}", a, b);
(a, b) = (b, a); // tuple swap
println!("after: a={}, b={}", a, b);
}Tuples and the Copy Trait
A tuple implements Copy if and only if every element type implements Copy. If any element is a String or another heap-allocated type, the tuple as a whole does not implement Copy and will be moved on assignment.
fn main() {
// (i32, f64) is Copy — both fields are Copy types
let t1 = (1, 2.0);
let t2 = t1; // t1 is copied, not moved
println!("{:?} {:?}", t1, t2); // both are valid
// (i32, String) is NOT Copy — String is not Copy
let t3 = (42, String::from("hello"));
let t4 = t3; // t3 is moved into t4
// println!("{:?}", t3); // compile error: t3 was moved
println!("{:?}", t4);
}Tuples vs Structs
Both tuples and structs group multiple values together. The choice between them depends on whether the fields need names.
Feature | Tuple | Struct |
|---|---|---|
Field access | By index: | By name: |
Readability | Low for many fields | High — names document intent |
Syntax overhead | Minimal — no declaration needed | Requires a |
Best for | Small ad-hoc groupings | Domain entities with meaning |
Pattern matching | Yes | Yes |
Display | Debug only | Debug only (unless you impl Display) |
Practical Size Limit
Rust automatically implements standard traits (like Debug, Clone, PartialEq) for tuples up to 12 elements. You can create tuples with more than 12 fields, but they lose most trait implementations and become awkward to use. If you need more than 4 or 5 fields, a struct is almost always the better choice.
Common Use Cases
Returning multiple values from a function without defining a struct.
Grouping a status code with a response body:
(StatusCode, ResponseBody).Swapping two variables without a temporary.
Pattern matching on a pair of conditions simultaneously.
Storing a small, unnamed, short-lived grouping of values.
Summary
Tuples hold a fixed number of values of potentially different types.
Access elements with
.0,.1, etc., or destructure withlet (a, b, c) = tup;.The unit type
()is the empty tuple and is the implicit return type of functions with no explicit return.Tuples implement
Copyonly when all their elements do.Use
{:?}(Debug) to print tuples — they do not implementDisplay.Prefer named structs over tuples when the meaning of each field is not obvious from context.
Rust implements standard traits for tuples up to 12 elements.