|$ curl https://forge-ai.dev/api/markdown?path=docs/python/tuples
$cat docs/python-—-tuples.md
updated Recently·12 min read·published

Python — Tuples

PythonBeginner to Intermediate
Introduction

A tuple is an ordered, immutable sequence of elements. Think of it as a list that cannot change — no appends, no removals, no item reassignment. Tuples are defined with parentheses () and can hold elements of any type, mixed freely.

Despite their simplicity, tuples are one of Python's most versatile data structures. They serve as lightweight records, dictionary keys, function return values, and the backbone of multiple assignment. Understanding when to reach for a tuple over a list is a hallmark of idiomatic Python.

Tuple Creation

Tuples can be created with literals, the tuple() constructor, or via comprehension-like generator expressions. The most common way is the parenthesized literal.

tuple_creation.py
Python
1# Tuple literals — parentheses optional in many contexts
2empty = () # empty tuple
3point = (3, 4) # two-element tuple
4rgb = (255, 128, 0) # three elements
5mixed = (1, "hello", 3.14) # heterogeneous elements
6nested = ((1, 2), (3, 4)) # nested tuples
7
8# Single-element tuple — the trailing comma is REQUIRED
9single = (42,) # this is a tuple
10not_a_tuple = (42) # this is just the int 42
11print(type(single)) # <class 'tuple'>
12print(type(not_a_tuple)) # <class 'int'>
13
14# tuple() constructor — from any iterable
15from_list = tuple([1, 2, 3]) # → (1, 2, 3)
16from_string = tuple("abc") # → ('a', 'b', 'c')
17from_range = tuple(range(5)) # → (0, 1, 2, 3, 4)
18from_generator = tuple(x**2 for x in range(4)) # → (0, 1, 4, 9)
19
20# Parentheses can be omitted in assignments (tuple packing)
21packed = 1, 2, 3 # → (1, 2, 3) — implicit tuple
22print(type(packed)) # <class 'tuple'>
23
24# Single-element packing also needs comma
25packed_single = 42,
26print(packed_single) # → (42,)
27
28# Empty tuple — no ambiguity here
29empty2 = tuple() # → ()

warning

The trailing comma is the defining feature of a tuple, not the parentheses. (42) is an integer because parentheses are used for grouping in expressions. (42,) is a tuple. This is one of the most common beginner mistakes in Python.
Immutability — What It Really Means

Once created, a tuple's structure cannot change: you cannot add, remove, or replace elements. However, if a tuple contains a mutable object (like a list), that object itself can be modified. This is called shallow immutability.

immutability.py
Python
1# Immutable structure — these all fail
2t = (1, 2, 3)
3# t[0] = 99 # TypeError: 'tuple' object does not support item assignment
4# t.append(4) # AttributeError: 'tuple' object has no attribute 'append'
5# t.pop() # AttributeError
6# del t[0] # TypeError
7
8# But mutable contents CAN be modified (shallow immutability)
9t = (1, [10, 20], 3)
10t[1].append(30) # OK — we're modifying the list, not the tuple
11print(t) # → (1, [10, 20, 30], 3)
12t[1][0] = 99 # OK — same reasoning
13print(t) # → (1, [99, 20, 30], 3)
14
15# Why this matters — hashability
16# Tuples are hashable ONLY if all their elements are hashable
17hashable = (1, 2, 3) # OK — all ints are hashable
18print(hash(hashable)) # → some integer
19
20# unhashable = (1, [2, 3], 4) # TypeError: unhashable type: 'list'
21# because lists are mutable and therefore unhashable
22
23# Immutability enables tuples to be used as dictionary keys
24d = {(1, 2): "point-a"} # OK
25# d = {[1, 2]: "point-a"} # TypeError — lists aren't hashable
26
27# Tuples are also used internally — function *args are a tuple
28def show_args(*args):
29 print(type(args)) # <class 'tuple'>
30 print(args) # immutable positional arguments
31
32show_args(1, 2, 3) # → (1, 2, 3)

info

The hashability of tuples (when their contents are also hashable) makes them essential as dictionary keys. This is the primary reason tuples exist as a separate type from lists — immutability enables __hash__.
Indexing & Slicing

Tuples support the same indexing and slicing operations as lists. Since they are sequences, everything that reads from a list also works on a tuple.

indexing_slicing.py
Python
1t = (10, 20, 30, 40, 50)
2
3# Indexing — zero-based, negative wraps around
4print(t[0]) # → 10
5print(t[-1]) # → 50 (last element)
6print(t[-2]) # → 40 (second-to-last)
7
8# Slicing — returns a NEW tuple (always a copy)
9print(t[1:4]) # → (20, 30, 40) indices 1..3
10print(t[:3]) # → (10, 20, 30) start to index 2
11print(t[2:]) # → (30, 40, 50) index 2 to end
12print(t[::2]) # → (10, 30, 50) every other element
13print(t[::-1]) # → (50, 40, 30, 20, 10) reversed copy
14
15# Membership testing — O(n)
16print(30 in t) # → True
17print(99 in t) # → False
18print(99 not in t) # → True
19
20# Length
21print(len(t)) # → 5
22
23# Concatenation and repetition — new tuples
24print(t + (60, 70)) # → (10, 20, 30, 40, 50, 60, 70)
25print(t * 2) # → (10, 20, 30, 40, 50, 10, 20, 30, 40, 50)
26print((0,) * 5) # → (0, 0, 0, 0, 0)
27
28# Comparison — lexicographic, element by element
29print((1, 2, 3) < (1, 2, 4)) # → True
30print((1, 2) < (1, 2, 3)) # → True (shorter is "less")
31print((1, 3) < (1, 2, 4)) # → False (3 > 2 at index 1)
Tuple Unpacking (Destructuring)

Tuple unpacking lets you assign each element of a tuple to a separate variable in one line. This is one of Python's most elegant features and extends far beyond tuples — it works with any iterable.

unpacking.py
Python
1# Basic unpacking
2point = (3, 4)
3x, y = point
4print(x, y) # → 3 4
5
6# Swapping variables — the Pythonic way
7a, b = 10, 20
8a, b = b, a
9print(a, b) # → 20 10
10
11# Unpacking any iterable — not just tuples
12first, second, third = [1, 2, 3]
13print(first, second, third) # → 1 2 3
14
15# Star (*) unpacking — Python 3+
16head, *tail = (1, 2, 3, 4, 5)
17print(head) # → 1
18print(tail) # → [2, 3, 4, 5] (always a list!)
19
20*beginning, last = (1, 2, 3, 4, 5)
21print(beginning) # → [1, 2, 3, 4]
22print(last) # → 5
23
24first, *middle, last = (1, 2, 3, 4, 5)
25print(first, middle, last) # → 1 [2, 3, 4] 5
26
27# Star unpacking in function calls — spread operator
28def add(a, b, c):
29 return a + b + c
30
31nums = (10, 20, 30)
32print(add(*nums)) # → 60 — same as add(10, 20, 30)
33
34# Nested unpacking
35data = (1, (2, 3), 4)
36a, (b, c), d = data
37print(a, b, c, d) # → 1 2 3 4
38
39# Ignoring values with underscore
40_, important, *_ = (1, 2, 3, 4, 5)
41print(important) # → 2
42
43# Multiple return values — functions "return" tuples
44def min_max(items):
45 return min(items), max(items)
46
47low, high = min_max([3, 1, 7, 2, 9])
48print(low, high) # → 1 9
49
50# For loop unpacking — iterate over pairs
51pairs = [(1, "a"), (2, "b"), (3, "c")]
52for num, letter in pairs:
53 print(f"{letter}: {num}")
54# → a: 1
55# → b: 2
56# → c: 3
57
58# Enumerate returns tuples — already unpacking
59for idx, val in enumerate(["x", "y", "z"]):
60 print(idx, val)

best practice

Star unpacking is vastly more readable than indexing or slicing when you need to split a sequence. Use head, *tail = seq instead of head, tail = seq[0], seq[1:]. The star expression also works in list/tuple literals: [1, 2, *other_list].
Named Tuples (collections.namedtuple)

A namedtuple is a factory function that creates tuple subclasses with named fields. You get the immutability and memory efficiency of tuples with the readability of named attributes — perfect for lightweight data records.

namedtuple.py
Python
1from collections import namedtuple
2
3# Defining a named tuple type
4Point = namedtuple("Point", ["x", "y"])
5# ^ class name ^ type name ^ field names
6
7# Creating instances
8p1 = Point(3, 4)
9p2 = Point(x=10, y=20)
10
11# Access by index (tuple behavior)
12print(p1[0]) # → 3
13print(p1[1]) # → 4
14
15# Access by attribute (named behavior)
16print(p1.x) # → 3
17print(p1.y) # → 4
18
19# Unpacking works
20x, y = p1
21print(x, y) # → 3 4
22
23# Immutable — cannot set attributes
24# p1.x = 99 # AttributeError: can't set attribute
25
26# Useful for return values
27def divide(a, b):
28 Result = namedtuple("Result", ["quotient", "remainder"])
29 return Result(a // b, a % b)
30
31res = divide(10, 3)
32print(res.quotient) # → 3
33print(res.remainder) # → 1
34q, r = divide(10, 3) # unpacking still works
35
36# Named tuple fields and properties
37print(Point._fields) # → ('x', 'y') — tuple of field names
38print(p1._asdict()) # → {'x': 3, 'y': 4}
39
40# _replace — create a NEW tuple with some fields changed
41p3 = p1._replace(x=99)
42print(p1) # → Point(x=3, y=4) — original unchanged
43print(p3) # → Point(x=99, y=4) — new tuple
44
45# _make — create from iterable
46data = [5, 6]
47p4 = Point._make(data)
48print(p4) # → Point(x=5, y=6)
49
50# Field defaults (Python 3.7+)
51Node = namedtuple("Node", ["value", "left", "right"], defaults=(None, None))
52root = Node(42)
53print(root) # → Node(value=42, left=None, right=None)
54
55# Docstring and module info
56print(Point.__doc__) # → Point(x, y)
57print(Point.x.__doc__) # → Alias for field number 0
58
59# Renaming invalid field names automatically
60# NamedTuple = namedtuple("Bad", ["class", "for", "def"], rename=True)
61# → Bad(class_=1, for_=2, def_=3)
62
63# Type hints with NamedTuple (Python 3.6+)
64from typing import NamedTuple
65
66class Employee(NamedTuple):
67 name: str
68 id: int
69 active: bool = True
70
71e = Employee("Alice", 1001)
72print(e.name) # → Alice
73print(e.active) # → True

info

Use namedtuple when you need a simple, immutable data container without the overhead of a full class. For Python 3.7+, dataclasses is an alternative if you need mutability or more features; for 3.6+, the typing NamedTuple variant adds type annotation support.
Tuple Methods

Tuples have only two built-in methods — count and index. Since tuples are immutable, there are no modifying methods like append, extend, or sort.

tuple_methods.py
Python
1t = (1, 2, 3, 2, 4, 2, 5)
2
3# count — how many times a value appears
4print(t.count(2)) # → 3
5print(t.count(1)) # → 1
6print(t.count(99)) # → 0
7
8# index — first position of a value (raises ValueError if not found)
9print(t.index(3)) # → 2
10print(t.index(2)) # → 1 (first occurrence)
11print(t.index(2, 2)) # → 3 (start searching at index 2)
12print(t.index(2, 4)) # → 5 (start searching at index 4)
13# t.index(99) # ValueError: tuple.index(x): x not in tuple
14
15# Helper pattern — safe index with default
16def safe_index(tup, value, default=-1):
17 try:
18 return tup.index(value)
19 except ValueError:
20 return default
21
22print(safe_index(t, 2)) # → 1
23print(safe_index(t, 99, -1)) # → -1
24
25# That's it — no other methods. Compare to lists which have:
26# append, extend, insert, remove, pop, clear, sort, reverse
27# Tuples don't need them — they can't change!
📝

note

Because tuples have fewer methods than lists, they are slightly lighter in memory and faster to create. The tradeoff is intentional — tuples signal that the data should not change, and the restricted API enforces that contract.
When to Use Tuples

Tuples shine in specific scenarios where immutability, hashability, or memory efficiency matter. Here are the most common use cases in idiomatic Python.

use_cases.py
Python
1# 1. Fixed records / lightweight structs
2person = ("Alice", 30, "Engineer")
3# vs: person = {"name": "Alice", "age": 30, "job": "Engineer"}
4# Tuple is lighter, but you lose named access
5
6# 2. Multiple return values from functions
7def divide_with_remainder(a, b):
8 return a // b, a % b # returns a tuple
9
10quotient, remainder = divide_with_remainder(10, 3)
11
12# 3. Dictionary keys — tuples are hashable
13locations = {
14 (40.7128, -74.0060): "New York",
15 (34.0522, -118.2437): "Los Angeles",
16 (51.5074, -0.1278): "London",
17}
18# Lists cannot be dict keys — tuples enable multi-key lookups
19
20# 4. Function arguments (*args is a tuple)
21def log(message, *tags):
22 # tags is a tuple — immutable, always fixed during call
23 for tag in tags:
24 print(f"[{tag}] {message}")
25
26log("server started", "info", "system")
27
28# 5. Immutable sequences as configuration constants
29DEFAULT_CONFIG = ("localhost", 8080, False)
30
31def connect(host, port, use_tls):
32 pass
33
34connect(*DEFAULT_CONFIG) # unpack as arguments
35
36# 6. Database rows — fetched as tuples
37# cursor.fetchall() returns list of tuples
38# Each row: (id, name, email)
39
40# 7. Grouping related values without a class
41from datetime import date
42
43def get_date_parts():
44 today = date.today()
45 return today.year, today.month, today.day
46
47y, m, d = get_date_parts()
48
49# 8. Format strings — safe positional arguments
50print("Year: %d, Month: %d, Day: %d" % (y, m, d))
51
52# 9. Storing heterogeneous but fixed-length data
53# CSV row, GPS coordinate, RGB color, bounding box
54rgb = (255, 128, 0) # always 3 values
55bbox = (10, 20, 100, 200) # always 4 values

best practice

A good rule of thumb: use a tuple when the number of elements is fixed and each position has a specific meaning (a record), use a list when the number of elements can vary (a collection). Tuples are also the default for function arguments and multiple return values — Python uses them automatically.
Tuple vs List — Tradeoffs

Choosing between tuple and list affects performance, semantics, and API design. Here's how they compare across key dimensions.

CriterionTupleList
MutabilityImmutableMutable
HashableYes (if elements are)No
Dict KeyYesNo
Memory~16-40% smallerLarger (over-allocation)
Creation SpeedFasterSlower
Methodscount, index (2)append, extend, insert, remove, pop, clear, sort, reverse, count, index (10+)
Use CaseFixed structure, records, keysVariable-length collections
IterationComparable speedComparable speed
Syntax(1, 2, 3)[1, 2, 3]
tradeoffs.py
Python
1# Memory efficiency — tuples don't over-allocate
2import sys
3
4lst = [1, 2, 3, 4, 5]
5tup = (1, 2, 3, 4, 5)
6
7print(sys.getsizeof(lst)) # → 120 (varies by Python version)
8print(sys.getsizeof(tup)) # → 80 (no over-allocation overhead)
9
10# Lists pre-allocate extra capacity for future appends
11# Tuples allocate exactly what they need — no more, no less
12
13# Creation speed
14import timeit
15
16t_list = timeit.timeit("[1, 2, 3, 4, 5]", number=10_000_000)
17t_tuple = timeit.timeit("(1, 2, 3, 4, 5)", number=10_000_000)
18
19print(f"list creation: {t_list:.3f}s")
20print(f"tuple creation: {t_tuple:.3f}s")
21# Tuples are typically 1.5-2x faster to create
22
23# Semantics matter more than performance
24# Use tuple to signal: "this data should not change"
25DEFAULT_PORT = (8080, "http")
26
27# Use list to signal: "this data may grow or shrink"
28active_connections = [8080, 3000, 443]

info

The memory and speed advantages of tuples are real, but they rarely matter in most applications. The primary reason to choose a tuple over a list is semantic: tuples signal immutability and enable use as dictionary keys. Write for clarity first, optimize only when profiling tells you to.
$Blueprint — Engineering Documentation·Section ID: PYTHON-TUPLES·Revision: 1.0