Tuples & Named Tuples
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI 1.17.1, Scala 3.10.0 and JDK 26 on 2026-10-15. A tuple groups a fixed number of values of possibly different types into one value without declaring a class; a named tuple adds a name to each element. Tuples are the quickest way to return two or three results from a method, to carry a key with a value, and to pattern match on several values at once, while named tuples give those anonymous bundles readable field names. The main differences from Scala 2 are that tuples of any size are allowed, that tuples have a rich set of operations in the standard library, that lambdas can take a tuple apart by parameter (untupling), and that elements can be named. Named tuples became stable in Scala 3.7 (they were experimental from 3.5) and therefore work on the 3.9 LTS line but not on the 3.3 LTS line. Everything else on this page works on Scala 3.3 LTS too. For naming and modelling data with a class see Case Classes; the patterns used here are covered in Pattern Matching; the types involved in Type Hierarchy & Matchable.
Creating tuples and reading their elements
A tuple is written as a comma-separated list in parentheses. Its type is the same list of types:
(String, Int, Boolean) is the type of ("Refugio", 2100, true). A pair can also be written with the arrow ->,
which comes from ArrowAssoc and produces the same Tuple2. A tuple is immutable and has value equality, so two
tuples with equal elements are equal.
Elements are read with apply and a constant, zero-based index: hut(0). The compiler knows the index at compile
time and gives the call the precise element type, so no cast is needed. The Scala 2 accessors _1, _2, … (one-based)
still exist for Tuple1 to Tuple22. If the index is not a constant, the result type is the union of all element
types. A tuple can also be taken apart in a val pattern, which is usually the clearest way.
//> using scala 3.10.0
@main def tuples(): Unit =
val hut = ("Refugio", 2100, true) // (String, Int, Boolean)
val pair = "Pico" -> 3012 // (String, Int), built with ->
val name: String = hut(0) // apply(0): type is String
val elevation: Int = hut(1)
println(s"$name at $elevation m")
println(hut._3) // Scala 2 style accessor still works
println(pair.swap) // Tuple2 only: (3012,Pico)
println(pair.copy(_2 = 3100))
println(hut.productArity)
println(hut.toString)
def pick(i: Int) = hut(i) // not a constant: only the union is known
val any: String | Int | Boolean = pick(1)
println(any)
val (n, e, open) = hut // pattern definition
println(s"$n $e $open")
assert(hut == ("Refugio", 2100, true))
Refugio at 2100 m
true
(3012,Pico)
(Pico,3100)
3
(Refugio,2100,true)
2100
Refugio 2100 true
Source: Scala Tour: Tuples
Tuples are checked statically. An index outside the tuple, a missing accessor and a wrong element count are compile errors, not runtime errors:
//> using scala 3.10.0
@main def e(): Unit =
val t = (1, 2, 3)
println(t(3))
[error] ./X1.scala:5:13
[error] index out of bounds: 3
[error] println(t(3))
[error] ^
//> using scala 3.10.0
@main def e(): Unit =
val t = (1, 2, 3)
println(t._4)
[error] ./X2.scala:5:11
[error] value _4 is not a member of (Int, Int, Int) - did you mean t._1? or perhaps t._2 or t._3?
The compiler also printed a long "Match type reduction failed" warning ahead of the first error (it comes from the
Tuple.Elem match type that apply uses); the line that matters is the index out of bounds error. Note the two index
conventions: apply is zero-based and _1 is one-based.
Tuple types, Product and the runtime representation
Tuple1[A] to Tuple22[...] are ordinary generic case classes in the scala package. Tuple2 is specialized on primitive
element types, which avoids boxing: the class of (1, 2) is scala.Tuple2$mcII$sp at run time. All of them extend Product, so a tuple
has productArity, productElement(i) and productIterator, and an unnamed tuple reports the element names _1,
_2, and so on from productElementNames. Case classes are products too, which is why a tuple and a case class can both be
used with the generic derivation tools covered in Type Class
Derivation. As case classes, the TupleN classes up to 22 have copy, and Tuple2 also has swap.
All tuple types, of any arity, extend the trait scala.Tuple (a sealed Product whose members are the operations in the
next section). The empty tuple is the object EmptyTuple, and a non-empty tuple is a cons cell *:, which is why
(Int, String) is also written Int *: String *: EmptyTuple. This construction is what lets the standard library define
tuple operations by recursion over the type.
Operations on tuples
scala.Tuple defines the operations as methods whose result types are computed by match types over the tuple types (the
Tuple.Concat, Tuple.Head and Tuple.Size types are visible in the API). The result of each is therefore as precise as
the input: appending a Double to a (String, Int) gives a (String, Int, Double), not a Tuple.
| Member | Result |
|---|---|
|
number of elements, as a constant type for |
|
first, last, all but the first, all but the last |
|
the element at a constant zero-based index |
|
prefix, suffix, both; |
|
concatenation of two tuples |
|
prepend ( |
|
apply a polymorphic function to every element |
|
pair up the elements of two tuples of equal length |
|
a |
|
reversed copy |
|
build a tuple from an array or from any |
|
build a tuple from a case class, keeping the element types |
Because the elements have different types, map takes a polymorphic function, [T] => (x: T) => f(x) in the
syntax of Dependent & Polymorphic
Function Types. toList loses the individual types and returns a List of their union.
//> using scala 3.10.0
@main def tupleOps(): Unit =
val t = ("Refugio", 2100, true)
println(t ++ (1.5, 'x')) // concatenation
println(t.size)
println(t.head)
println(t.tail)
println(t.take(2))
println(t.drop(2))
println(t.splitAt(1))
println(t :* 99) // append
println(0 *: t) // prepend
println(t.toList) // List[String | Int | Boolean]
println((1, 2, 3).map([T] => (x: T) => List(x)))
println((1, "a", 2.0).zip((true, false, true)))
println(Tuple.fromArray(Array[Any](1, "x")))
println(EmptyTuple)
val big = (1 to 30).foldRight(EmptyTuple: Tuple)((i, acc) => i *: acc)
println(big.size)
println(big(29))
val t23 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
println(t23(22))
println(t23.getClass.getName)
println(t.getClass.getName)
(Refugio,2100,true,1.5,x)
3
Refugio
(2100,true)
(Refugio,2100)
(true)
((Refugio),(2100,true))
(Refugio,2100,true,99)
(0,Refugio,2100,true)
List(Refugio, 2100, true)
(List(1),List(2),List(3))
((1,true),(a,false),(2.0,true))
(1,x)
()
30
30
23
scala.runtime.TupleXXL
scala.Tuple3
Source: Scala API: scala.Tuple
Arities above 22
Scala 2 had exactly the classes Tuple1 to Tuple22, and a 23-element tuple did not compile. Scala 3 dropped the limit.
A tuple with more than 22 elements is erased to scala.runtime.TupleXXL, which stores its elements in an array and
supports the same generic operations (concatenation, indexing and so on), as the example above shows with a 30-element
tuple built by *: and a 23-element tuple literal. The cost is the array: TupleXXL operations copy it, so very large
tuples are not a substitute for a collection. The same page of the reference removes the limit for function
types, which beyond Function22 are erased to scala.runtime.FunctionXXL.
The type-level view is available through the Tuple object. Its match types describe the results of the operations and can be used to
write your own:
//> using scala 3.10.0
@main def tupleTypes(): Unit =
type Elems = Tuple.Concat[(Int, String), (Boolean, Double)]
val c: Elems = (1, "a", true, 2.0)
val h: Tuple.Head[Elems] = 7
val sz: Tuple.Size[Elems] = 4
val u: Tuple.Union[Elems] = "any of the element types"
println((c, h, sz, u))
def sumInts(t: Tuple): Int = t match
case EmptyTuple => 0
case (x: Int) *: rest => x + sumInts(rest)
case _ *: rest => sumInts(rest)
println(sumInts((1, "two", 3, 4.0, 5)))
((1,a,true,2.0),7,4,any of the element types)
9
sumInts recurses over the tuple with the *: pattern; the compiler accepts the EmptyTuple and *: cases as
exhaustive because those are the only two kinds of tuple.
Source: Scala API: scala.Tuple object
Pattern matching on tuples
A tuple pattern (p1, p2, ...) matches a tuple of the same length and matches each element against its sub-pattern.
It can appear in a match, in a val definition, in the generator of a for loop, and as the single case of an
anonymous function. The for and val forms use irrefutable patterns, so they are checked at compile time. The
mismatch between a three-element pattern and a pair is an error, not a runtime MatchError:
//> using scala 3.10.0
@main def e(): Unit =
val t = (1, "a")
val (a, b, c) = t
[warn] ./X9.scala:5:19
[warn] pattern's type (Any, Any, Any) does not match the right hand side expression's type (Int, String)
[warn]
[warn] If the narrowing is intentional, this can be communicated by adding `.runtimeChecked` after the expression,
[warn] which may result in a MatchError at runtime.
[warn] This patch can be rewritten automatically under -rewrite -source 3.8-migration.
[warn] val (a, b, c) = t
[warn] ^
[error] ./X9.scala:5:7
[error] Unreachable case
[error] val (a, b, c) = t
[error] ^
The runtimeChecked suggestion comes from SIP-57 (stable in 3.8); it is the explicit way to say that a refutable pattern in
a val is intentional, and it is covered in Extractors &
Pattern Bindings. The next example collects the tuple patterns that work, together with parameter untupling.
Parameter untupling
A function that takes one tuple parameter can be written as a function with several parameters. If the expected type
is ((A, B)) => C and the lambda has two parameters, Scala 3 wraps it so that the incoming pair is taken apart and its
elements passed on. In Scala 2 this needed a case clause: xs map { case (x, y) => x + y }, which wrongly suggests that
the match could fail. The adaptation applies to the lambda’s parameter list, not to function values: a
combiner: (Int, Int) => Int stored in a val is not accepted where a function from pairs is expected, and has to be
passed as combiner.tupled. A conversion in scope cannot subvert untupling, because untupling is tried before implicit
conversions. The closely related auto-tupling of an argument list, f(1, 2) for def f(t: (Int, Int)), is still
accepted by 3.10.0 and printed no warning even with -source:future -Werror in the check run for this page; do not rely on
it in new code, because it reads like a call with two arguments.
//> using scala 3.10.0
@main def matching(): Unit =
val trails = List(("Aneto", 3404), ("Collarada", 2886), ("Tuca", 3062))
for (name, h) <- trails do println(s"$name $h") // pattern in a generator
trails.foreach:
case (name, h) if h > 3000 => println(s"$name is a three-thousander")
case (name, _) => println(s"$name is lower")
def describe(t: (Int, String)): String = t match
case (0, s) => s"zero $s"
case (n, "x") => s"x $n"
case (n, s) => s"$n $s"
println(List((0, "a"), (2, "x"), (3, "q")).map(describe))
// parameter untupling: a lambda with two parameters accepts one pair
println(trails.map((n, h) => s"$n:$h"))
val m = Map("a" -> 1, "b" -> 2)
println(m.map((k, v) => (k.toUpperCase, v * 10)))
println(m.foldLeft(0)((acc, kv) => acc + kv._2))
val f: ((String, Int)) => String = (n, h) => s"$n/$h"
println(f(("x", 1)))
Aneto 3404
Collarada 2886
Tuca 3062
Aneto is a three-thousander
Collarada is lower
Tuca is a three-thousander
List(zero a, x 2, 3 q)
List(Aneto:3404, Collarada:2886, Tuca:3062)
Map(A -> 10, B -> 20)
3
x/1
Named tuples
A named tuple labels its elements. The syntax is the one of parameters and arguments: name: Type in the type,
name = value in the value. The names are part of the static type, so you select a field with hut.name and the
compiler checks the name and the field type. A name is never stored: at run time a named tuple is the same object as
the unnamed tuple of its values.
//> using scala 3.10.0
type Hut = (name: String, elevation: Int, beds: Int)
def summary(lengths: List[Double], ascents: List[Int]): (count: Int, km: Double, ascent: Int) =
(count = lengths.size, km = lengths.sum, ascent = ascents.sum)
@main def named(): Unit =
val hut: Hut = (name = "Refugio", elevation = 2100, beds = 40)
println(hut) // names are not part of toString
println(hut.name + " has " + hut.beds + " beds")
println(hut(0)) // positional access still works
val s = summary(List(5.5, 4.5), List(300, 200))
println(s.km)
val (n, e, b) = hut // positional pattern
println((n, e, b))
val (name = nm, beds = bd) = hut // named pattern: a subset, in any order
println(nm + " " + bd)
hut match
case (name = "Refugio", beds = bs) => println(s"Refugio with $bs beds")
case _ => println("another hut")
val plain: (String, Int, Int) = hut // named -> unnamed: allowed
val back: Hut = plain // unnamed -> named: allowed
println(back == hut)
println(hut.toTuple)
(Refugio,2100,40)
Refugio has 40 beds
Refugio
10.0
(Refugio,2100,40)
Refugio 40
Refugio with 40 beds
true
(Refugio,2100,40)
Note that toString does not show the names, and that back == hut is true: equality compares the values only. Stable
named tuples need no import from 3.7 on; before that they were experimental and needed the experimental language import (scala.language.experimental.namedTuples).
Source: Scala 3 Reference: Named Tuples (SIP-58, Scala Improvement Proposals: SIP-58; announced in Scala 3.7.0)
Restrictions and error messages
A tuple is either fully named or fully unnamed, and names must be unique. The order of names is part of the type, so
(a: Int, b: Int) and (b: Int, a: Int) are incompatible. Selecting a name that is not there is an ordinary error. All
of these are real compiler messages from 3.10.0:
val x: (a: Int, b: Int) = (b = 1, a = 2)
Found: (b : Int, a : Int)
Required: (a : Int, b : Int)
val x = (a = 1, 2)
Illegal combination of named and unnamed tuple elements
val x = (a = 1, a = 2)
Duplicate tuple element name
h.height // h = (name = "R", beds = 4)
value height is not a member of (name : String, beds : Int)
val (height = z) = h
No element named `height` is defined in selector type (name : String, beds : Int)
Conformance and conversions
The rule mirrors named and positional arguments: a regular tuple (T1, ..., Tn) is a subtype of every named tuple
(n1: T1, ..., nn: Tn) with the same element types, so you can always assign an unnamed tuple where a named one is
expected, for example val laura: Person = ("Laura", 25). The other direction forgets the names with toTuple, and the
compiler inserts .toTuple on its own when the expected type is a regular tuple. That implicit step happens at one level only:
it does not work inside a type constructor such as List:
//> using scala 3.10.0
type Person = (name: String, age: Int)
@main def e(): Unit =
val bob: Person = (name = "Bob", age = 33)
val persons: List[Person] = List(bob)
val pairs: List[(String, Int)] = persons
[error] ./err/X11.scala:8:36
[error] Found: (persons : List[Person])
[error] Required: List[(String, Int)]
[error] val pairs: List[(String, Int)] = persons
[error] ^^^^^^^
The fix is an explicit persons.map(_.toTuple).
Pattern matching on named tuples
A pattern on a named tuple may be positional, as with a plain tuple, or named. A named pattern mentions only the names it
needs, in any order, and a name that does not exist is an error (shown above). Named patterns are not limited to tuples:
a case class pattern accepts the field names too, and an unapply that returns a named tuple makes the extractor’s
fields available by name. A regular tuple, or any other selector type, can only be matched with positional patterns.
//> using scala 3.10.0
case class Trail(name: String, km: Double, ascent: Int)
type Stats = (count: Int, km: Double)
// an extractor whose unapply returns a named tuple, usable with named patterns
object Parts:
def unapply(s: String): Option[(left: String, right: String)] =
s.split("-", 2) match
case Array(l, r) => Some((left = l, right = r))
case _ => None
@main def more(): Unit =
val t = Trail("Aneto", 12.5, 1100)
t match
case Trail(name = n, ascent = a) => println(s"$n climbs $a m") // named patterns on a case class
"Refugio-Pico" match
case Parts(right = r) => println(r)
case _ => println("no match")
val trails = List(t, Trail("Tuca", 8.0, 700))
val totals: Stats = (count = trails.size, km = trails.map(_.km).sum)
println(totals)
val people: List[(name: String, age: Int)] = List(("Ana", 30), ("Leo", 12))
println(people.filter(p => p.age < 18))
println(people.map(p => p.name))
println(people.map(_.toTuple))
println(people.sortBy(_.age))
Aneto climbs 1100 m
Pico
(2,20.5)
List((Leo,12))
List(Ana, Leo)
List((Ana,30), (Leo,12))
List((Leo,12), (Ana,30))
Operations and NamedTuple.From
The NamedTuple object has extension methods that mirror those of Tuple (apply, head, tail, take, drop,
++, map, zip, reverse, toList, toArray, and so on), so every operation in the table above works on a named
tuple and keeps the names. zip requires both operands to have exactly the same names (a different set is a type error).
Internally, NamedTuple[N, V] is an opaque type alias of its value part V with the names N as a tuple of string literal
types, and it is a supertype, not a subtype, of V. That is the type-level reason why plain tuples convert to named ones for
free. NamedTuple.withNames gives an existing tuple names, and NamedTuple.Names and NamedTuple.DropNames read the two parts
of a type. A NamedTuple.From[C] is a compiler-built type: for a case class C it expands to the named tuple of the fields of
its first parameter list, so the field names and types can be reused without writing them twice. There is no built-in
conversion from a case class value to a named tuple; with Tuple.fromProductTyped and withNames you can build one, as below.
Libraries use From for query and mapping DSLs.
//> using scala 3.10.0
import scala.NamedTuple.{NamedTuple, withNames}
case class Trail(name: String, km: Double, ascent: Int)
@main def namedOps(): Unit =
val h = (name = "Refugio", elevation = 2100, beds = 40)
println(h.size)
println(h.head)
println(h.tail)
println(h ++ (area = "Pyrenees"))
println(h.take(2))
println(h.drop(2))
println(h.reverse)
println(h.toList)
println(h.zip((name = 1, elevation = 2, beds = 3)))
println(h.map([T] => (x: T) => Option(x)))
println(h.apply(1))
val raw = (1, 2).withNames[("a", "b")]
println(raw.b)
// NamedTuple.From turns the fields of a case class into a named-tuple type
val tr = Trail("Aneto", 12.5, 1100)
val asTuple: NamedTuple.From[Trail] =
Tuple.fromProductTyped(tr).withNames[NamedTuple.Names[NamedTuple.From[Trail]]]
println(asTuple.ascent)
def cost(x: (km: Double, ascent: Int)): Double = x.km + x.ascent / 100.0
println(cost((km = 3.0, ascent = 10)))
3
Refugio
(2100,40)
(Refugio,2100,40,Pyrenees)
(Refugio,2100)
(40)
(40,2100,Refugio)
List(Refugio, 2100, 40)
((Refugio,1),(2100,2),(40,3))
(Some(Refugio),Some(2100),Some(40))
2100
2
1100
3.1
Selectable fields and source incompatibilities
Selectable has a Fields type member that may be a named tuple. If a subclass sets Fields to a named tuple type, every
name in it becomes a legal selection that expands to selectDynamic("name") with the stated type. Query builders use this to
give a Table[Trail] the fields of Trail (type Fields = NamedTuple.Map[NamedTuple.From[T], Expr]). Selectable is covered in
Structural Types, Selectable & Dynamic.
Named tuples of arity one made two old forms ambiguous. The assignment in parentheses (age = 1) is now a one-element named
tuple, and an infix call c f (age = 1) now builds a tuple as the right operand rather than passing a named argument. Neither
is idiomatic, but old code may contain them.
Tuples or case classes
A tuple is a good fit for a result used right away: a pair returned from a method, a key and a value, a pattern that
groups two scrutinees. It has no name, no methods of its own and no defaults, and it prints as (a,b). A case class has all of those and documents the domain. A named
tuple sits in between: it is structural (any named tuple with the same names and types conforms), so it suits local
results and APIs where a one-use class would be noise, but it can’t carry behaviour and two different meanings with the same
field types are interchangeable, which a case class would keep apart. In a public API prefer a case class or a named tuple
alias (type Stats = (count: Int, km: Double)), not a bare (Int, Double).
Differences from Scala 2, Java and Kotlin
-
Scala 2: tuples stop at 22 elements; there is no
*:cons syntax, noTupleoperations (onlyTuple2.swap), no parameter untupling (acaseclause is required) and no named tuples.shapeless.HListwas the usual workaround for generic tuple code. -
Java: there are no tuples. Records (see Java: Records & Sealed Classes) are the nearest feature and correspond to case classes;
Map.Entryplays the role of a pair. A Scala tuple seen from Java is an instance ofscala.Tuple2and so on, with_1()and_2()methods. -
Kotlin: the standard library has only
PairandTriple(data classes) and thetoinfix function; destructuring works bycomponentN. There is no tuple of four or more and no named tuple; adata classis used instead. -
Python and TypeScript: Python tuples are dynamic and unpacked in assignment; TypeScript has labelled tuple types, which are only documentation, whereas Scala’s names are checked in selection and patterns.
Pitfalls
-
Zero-based
applyand one-based_1mix badly; pick one style per code base (applyand patterns read best). -
A non-constant index to
applyyields a union type, andtoListof a heterogeneous tuple is aListof a union: a signal that the data wants a class. -
toStringprints(1,2)without a space and never shows the names of a named tuple, so do not use it to display them. -
A tuple of more than 22 elements is a
TupleXXLbacked by an array; it is slower and probably a design smell. -
The implicit
.toTupledoes not reach insideList,Optionand other type constructors: convert withmap. -
Names are erased: reflection and Java code see a plain
Tuple2, and(x = 1.0, y = 2.0) == (1.0, 2.0)is true. -
A pair
(a, b)passed wheref(a, b)is expected may be auto-tupled silently; add a type to the parameter or use a case class to rule it out.
Where the books are outdated
The books consulted (see the section bibliography) were written for Scala 2 or the first years of Scala 3. They describe
tuples as Tuple1 to Tuple22 with a hard limit of 22 elements and _1 accessors only, list case (x, y) as the way to
take a pair apart in a lambda, and know nothing of the Tuple operations, of EmptyTuple and *:, or of named tuples,
which are the main data feature added in 3.7 (the Scala 3 reference and the Scala 3.7.0 announcement are the sources).
When a book asks for shapeless or a case class just to name three results, a named tuple is now often enough.