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

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

size, productArity

number of elements, as a constant type for size

head, last, tail, init

first, last, all but the first, all but the last

apply(i)

the element at a constant zero-based index

take(n), drop(n), splitAt(n)

prefix, suffix, both; n must be a constant

++

concatenation of two tuples

*:, :*

prepend (0 *: t) and append (t :* 99) one element

map(f)

apply a polymorphic function to every element

zip(u)

pair up the elements of two tuples of equal length

toList, toArray, toIArray

a List, Array[Object] or IArray of the elements (for toList, of the union of the element types)

reverse

reversed copy

Tuple.fromArray, Tuple.fromProduct

build a tuple from an array or from any Product

Tuple.fromProductTyped

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

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.

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).

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, no Tuple operations (only Tuple2.swap), no parameter untupling (a case clause is required) and no named tuples. shapeless.HList was 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.Entry plays the role of a pair. A Scala tuple seen from Java is an instance of scala.Tuple2 and so on, with _1() and _2() methods.

  • Kotlin: the standard library has only Pair and Triple (data classes) and the to infix function; destructuring works by componentN. There is no tuple of four or more and no named tuple; a data class is 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 apply and one-based _1 mix badly; pick one style per code base (apply and patterns read best).

  • A non-constant index to apply yields a union type, and toList of a heterogeneous tuple is a List of a union: a signal that the data wants a class.

  • toString prints (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 TupleXXL backed by an array; it is slower and probably a design smell.

  • The implicit .toTuple does not reach inside List, Option and other type constructors: convert with map.

  • 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 where f(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.