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.
# 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 copyAvoid 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
1. Why is prepending an element to an Elixir list an O(1) operation?
2. What is the time complexity of elem(tuple, index) for a tuple of any size?
3. Which data structure is the better fit for a function's return value like {:ok, result}?
4. What happens when you call put_elem/3 on an existing tuple?
5. Which operation on a list is O(n) rather than O(1)?
Was this page helpful?
You May Also Like
Maps in Elixir
Understand Elixir's key-value data structure, how to build, update, and pattern-match against maps, and when to reach for structs instead.
Keyword Lists
Learn Elixir's keyword list — an ordered list of atom-keyed tuples used heavily for options, configuration, and DSL-style function calls.
Comprehensions in Elixir
Learn Elixir's for special form — a concise syntax for looping over enumerables and generating new collections with filters, multiple generators, and pattern matching.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics