100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Lists and Tuples in Elixir

Learn how Elixir's two core sequential data structures — singly linked lists and fixed-size tuples — are represented in memory and when to use each one.

Data & CollectionsBeginner9 min readJul 10, 2026
Analogies

Lists and Tuples in Elixir

Elixir gives you two fundamentally different ways to group values in order: lists and tuples. A list, written as [1, 2, 3], is a singly linked list built out of head/tail pairs called cons cells — each element points to the rest of the list. A tuple, written as {1, 2, 3}, is a fixed-size, contiguous block of memory where every element sits at a known offset. Because they are stored so differently, they behave very differently under the hood: lists are cheap to grow at the front but expensive to index into, while tuples are cheap to read from any position but expensive to change.

🏏

Cricket analogy: A list is like a Test match scorecard built ball by ball — you can always prepend the next delivery to the front of the over, but finding the 47th ball bowled means walking through every entry before it, similar to how MS Dhoni's famous 2011 World Cup final innings can only be replayed delivery by delivery.

Working with Lists

Internally, an Elixir list is either the empty list [] or a cons cell of a head element and a tail that is itself a list, so [1, 2, 3] is really [1 | [2 | [3 | []]]]. This structure makes prepending with the | (cons) operator an O(1) operation, because you're just creating one new cell pointing at the existing list — nothing is copied. Appending to the end, or reading the element at index n, is O(n) because Elixir must walk the entire chain from the head. Functions like Enum.at/2, List.first/1, and hd/1 reflect this: hd/1 and List.first/1 are fast, but Enum.at(list, 500) is not.

🏏

Cricket analogy: Prepending to a list is like a substitute fielder being added to the XI at the last moment before the toss — one quick change at the front — while calling Enum.at on a long list is like having to review every over of a rain-delayed ODI innings just to find what happened at over 40.

Working with Tuples

Tuples store a fixed number of elements in contiguous memory with a size header, so elem(tuple, index) is an O(1) direct-offset read regardless of tuple size. This makes tuples ideal for fixed-shape data that you read often but rarely restructure, such as coordinates {x, y}, RGB colors {255, 0, 0}, or the ubiquitous {:ok, value} and {:error, reason} result tuples returned by functions like File.read/1 or Map.fetch/2. Because the tuple's tag (the first element, usually an atom like :ok or :error) is known at the call site, pattern matching on tuples in a case or function clause is both fast and expressive.

🏏

Cricket analogy: A tuple like {:out, "bowled"} mirrors an umpire's fixed decision format — outcome plus mode of dismissal — always two slots, read instantly the way a third umpire reviews a DRS verdict.

Choosing Between Lists and Tuples

The rule of thumb is: use lists for collections whose length varies and that you mostly traverse from the front (recursion, Enum pipelines, streaming), and use tuples for a fixed, small number of heterogeneous values you access by position, especially return values. Because tuples are contiguous, put_elem/3 or setelem-style updates must copy the entire tuple to produce a new one, so tuples are a poor fit for anything that grows or shrinks, or that you update repeatedly in a loop — that pattern belongs to a list, a map, or an accumulator passed through recursion.

🏏

Cricket analogy: Choosing a tuple for a fixed match result {team, runs, wickets} is right, but tracking every ball of an innings calls for a list, the way Cricinfo's ball-by-ball commentary grows with each delivery rather than being a fixed-size record.

Common List and Tuple Functions

The List module offers List.flatten/1 to collapse nested lists, List.first/1 and List.last/1 for endpoints, and List.delete/2 for removing a value, while the broader Enum module (map, filter, reduce, sort) works on any list because lists implement the Enumerable protocol. The Tuple module is deliberately small: Tuple.to_list/1, Tuple.insert_at/3, Tuple.delete_at/2, and elem/2 cover most needs, reflecting that tuples are meant to stay small and fixed rather than be manipulated like collections.

🏏

Cricket analogy: List.flatten mirrors merging separate innings-by-innings wicket lists from a multi-day Test into one flat fall-of-wickets list for the scorecard.

elixir
# Lists: prepend is O(1), append and indexing are O(n)
list = [1, 2, 3]
new_list = [0 | list]        # [0, 1, 2, 3] - cheap
appended = list ++ [4]       # [1, 2, 3, 4] - copies the left list
List.first(list)             # 1
Enum.at(list, 2)              # 3 - walks the list

# Tuples: fixed size, O(1) positional access
result = {:ok, %{id: 1, name: "Ada"}}

case result do
  {:ok, user} -> IO.puts("Loaded #{user.name}")
  {:error, reason} -> IO.puts("Failed: #{reason}")
end

elem(result, 0)               # :ok
put_elem(result, 0, :updated) # {:updated, %{id: 1, name: "Ada"}} - new tuple, full copy

Avoid growing a tuple inside a loop or repeatedly calling put_elem/3 on a large tuple — each call copies the entire tuple, turning an apparently simple update into an O(n) operation per iteration. If you need a mutable-feeling, appendable structure, reach for a list built with recursion and [head | acc], or a map.

The {:ok, result} / {:error, reason} tuple convention is idiomatic across the Elixir standard library and most community libraries. Pattern matching directly on the tag, as in {:ok, value} <- File.read(path), is the standard way to branch on success versus failure without exceptions.

  • Lists are singly linked chains of head/tail cons cells; prepending is O(1) but indexing is O(n).
  • Tuples are fixed-size, contiguous structures; elem/2 gives O(1) positional access.
  • Use lists for variable-length collections traversed with recursion or Enum.
  • Use tuples for small, fixed-shape data, especially {:ok, value} / {:error, reason} results.
  • put_elem/3 copies the whole tuple, so tuples are a poor fit for repeated updates.
  • The List and Enum modules cover list manipulation; the Tuple module stays intentionally minimal.
  • Pattern matching on tuple tags like :ok and :error is the idiomatic Elixir control-flow style.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ElixirStudyNotes#ListsAndTuplesInElixir#Lists#Tuples#Elixir#Choosing#DataStructures#StudyNotes#SkillVeris