🐍 Python Course

Tuples

3. Tuples

A tuple is an ordered, immutable sequence.

They are similar to lists, but the key difference is: a list is mutable, a tuple is immutable


Quick Reference: Tuple methods

MethodPurposeReturnsComplexityNotes
count(x)Count occurrences of valueintO(n)Returns 0 if not found
index(x)Find first matching valueIndexO(n)Raises ValueError if not found

Quick Reference: Common tuple operations

OperationPurposeReturnsComplexityNotes
tuple(iterable)Create tuple from iterableNew tupleO(n)Very common conversion
(x,)Create single-element tupleTupleO(1)Comma is required
t[i]Get item by indexElementO(1)Negative indexing supported
t[a:b]Slice tupleNew tupleO(k)k = slice size
len(t)Number of elementsintO(1)Very common in interviews
x in tMembership checkboolO(n)Linear scan
for x in tIterate through elementsIterator behaviorO(n) totalCommon iteration pattern
enumerate(t)Iterate with index + valueIteratorO(n) totalCommon in loops
t1 + t2Concatenate tuplesNew tupleO(n + m)Creates new object
t * nRepeat tupleNew tupleO(n¡k)Copies references
min(t)Smallest valueElementO(n)Built-in
max(t)Largest valueElementO(n)Built-in
sum(t)Sum numeric valuesNumberO(n)Built-in
a, b = tTuple unpackingVariablesO(n)Very common in practice
a, *rest = tExtended unpackingVariablesO(n)Useful for flexible assignment
hash(t)Hash immutable tupleintO(n)Only if all elements are hashable
t1 < t2Lexicographical comparisonboolO(n)Compared left to right

Example

point = (10, 20)

This means two important things:

Ordered

Every element has a stable position.

point[0]   # 10
point[1]   # 20

Just like lists.

Immutable

Unlike lists, tuples cannot be changed after creation.

This will fail:

point[0] = 99   # TypeError: 'tuple' object does not support item assignment

This is the defining feature of tuples.


1.3.2 Syntax

Standard syntax

coords = (4, 7)

Interview trap: single-element tuple

x = (5)   # Is just an integer in parentheses.
x = (5,)  # Is is single element tuple

1.3.3 Why tuples exist

Why use tuple instead of list? / Why do tuples exist?

This is not an academic question. Tuples solve real problems.

1. Fixed-size structure

Tuples communicate that the data will not grow.

# A point is always (x, y) - never grows to (x, y, z)
point = (10, 20)

# Using a list would suggest it might change:
point = [10, 20]  # Looks like you might .append() later

Why this matters:

  • API contracts: function says "I return a 3-tuple" = caller knows exactly what to expect
  • prevents bugs: if function accidentally appends, static analysis could catch tuple type error
  • semantic clarity: tuple = "fixed record", list = "growing collection"

2. Immutability prevents bugs

Tuples cannot be modified, so accidental mutations are impossible.

def get_rgb():
    return (255, 0, 0)

r, g, b = get_rgb()
r = 100  # This creates a NEW variable, doesn't modify the tuple

# Compare with list:
def get_rgb_list():
    return [255, 0, 0]

colors = get_rgb_list()
colors[0] = 100  # Modifies the list - may affect other code pointing to it

Why this matters:

  • shared references: if multiple variables reference the same list and one modifies it, all see the change
  • thread safety: immutable objects are inherently safe in multi-threaded code
  • caching: immutable tuples can be cached by Python (small tuples are cached), mutable lists cannot

3. Hashable = can be dictionary keys or set members

Lists cannot be dictionary keys because they're mutable. Tuples can (if contents are hashable).

# This works - tuple as key
cache = {
    (10, 20): "point A",
    (30, 40): "point B"
}

# This crashes - list not hashable
cache = {
    [10, 20]: "point A"  # TypeError: unhashable type: 'list'
}

Why this matters:

  • deduplication: set(points) with tuples removes duplicates instantly. Can't do this with lists
  • fast lookup: use tuple coordinates as dictionary keys for O(1) position lookup in graphs
  • caching positions: track visited nodes in graph algorithms using a set of tuples

Real example:

visited = set()  # Track visited grid positions
visited.add((0, 0))
if (1, 2) in visited:  # O(1) lookup
    ...

4. Safer semantic meaning

A tuple literally says "this data is a fixed record" to other developers.

def get_user():
    return ("Alice", 30, "alice@example.com")  # Tuple = fixed record, 3 specific fields

Compare to:

def get_user():
    return ["Alice", 30, "alice@example.com"]  # List = collection, might grow

Why this matters:

  • code readability: tuple communicates intent at a glance
  • prevents feature creep: "we return a 2-tuple for performance" is a contract
  • API stability: Python type checkers (mypy) will error if you return wrong tuple size

5. Can be used with multiple return unpacking

Tuples enable elegant destructuring at the call site.

# Tuple return enables this clean unpacking:
def get_stats(nums):
    return min(nums), max(nums), len(nums)

minimum, maximum, count = get_stats([1, 2, 3])

Why this matters:

  • readability: immediate clarity what's being returned
  • type safety: mypy knows exactly 3 values returned
  • ergonomics: unpacking is more readable than indexing [0], [1], [2]

Real-world example: Why tuples matter

# Coordinate-based cache using tuple as key
class Cache:
    def __init__(self):
        self.data = {}  # Maps coordinate tuples to cached results
    
    def get(self, x, y):
        # Can use (x, y) as key because tuples are hashable
        return self.data.get((x, y))
    
    def set(self, x, y, value):
        self.data[(x, y)] = value
    
    def is_cached(self, x, y):
        # Can use 'in' operator because key is hashable
        return (x, y) in self.data

cache = Cache()
cache.set(10, 20, "result")
if cache.is_cached(10, 20):  # O(1) lookup
    print(cache.get(10, 20))

With a list, none of this works because lists aren't hashable.


Strong answer summary:

  • fixed-size: communicates API contract
  • immutable: prevents accidental mutations and safer in multi-threaded code
  • hashable: enables use as dict key or set member
  • semantic clarity: tuple = fixed record, list = growing collection
  • enables unpacking: clean destructuring syntax

1.3.4 Tuple unpacking

This is one of the most important tuple topics.

Basic unpacking

point = (10, 20)
x, y = point

Now:

x = 10
y = 20

This is called unpacking.

Python assigns each element to a variable.

Why this is important

Tuple unpacking appears everywhere in real code.

Example

name, age = ("Ruben", 30)

Very common.

Function return unpacking

This is one of the biggest tuple use cases.

def get_user():
    return "Ruben", 30

name, age = get_user()

This is extremely common in Python.

Technically the function returns a tuple.


1.3.5 Swapping variables

A very famous Python tuple pattern.

a = 1
b = 2

a, b = b, a

Now:

a = 2
b = 1

This is elegant and highly interview-relevant.

Behind the scenes, Python uses tuple packing and unpacking.


1.3.6 Extended unpacking

Medior-level topic and often asked.

nums = (1, 2, 3, 4)

first, *middle, last = nums

Result

first = 1
middle = [2, 3]
last = 4

Very important to know.

Notice:

middle becomes a list, not a tuple.

That is an important subtlety.


1.3.7 Immutability in depth

This is the key concept.

Once created, the tuple structure cannot change.

You cannot:

  • assign by index
  • append
  • remove
  • sort in place

Example

point.append(3)   # AttributeError

Important nuance: nested mutables

This is a strong interview nuance.

A tuple itself is immutable, but it may contain mutable objects.

Example

data = ([1, 2], [3, 4])
data[0].append(99)       # ([1, 2, 99], [3, 4])

Important insight:

  • the tuple structure is immutable
  • the objects inside may still be mutable

1.3.8 Hashability and dictionary keys

This is extremely important.

Tuples can be dictionary keys if all elements are hashable.

Example

locations = {
    (10, 20): "home"
}

This is valid.

Why list cannot be key

{
    [10, 20]: "home"
}

This fails because lists are mutable and not hashable.

This comparison is frequently tested.


1.3.9 Important tuple methods

Tuples have fewer methods than lists because they are immutable.

count()

Count occurrences of a value.

t = (1, 2, 2, 3, 2)
count = t.count(2)   # 3

What it does

Counts how many times a value appears in the tuple.

colors = ("red", "blue", "red")
colors.count("red")  # 2

Complexity

O(n): Must scan the entire tuple.


index()

Returns the index of the first occurrence of the value.

t = (10, 20, 30, 20)
pos = t.index(20)  # 1

Raises error if not found

t = (1, 2, 3)
t.index(5)  # ValueError: tuple.index(x): x not in tuple

Complexity

O(n): Must search from the beginning.


Why tuples have few methods

Tuples are intentionally minimal because they are immutable.

You can't add/remove/modify elements, so methods like append(), remove(), pop(), sort() don't make sense.

This is by design — immutability = fewer operations = clearer intent.


1.3.10 Namedtuples

Namedtuples are a way to create lightweight, immutable objects with named fields instead of positional indexing.

They combine the best of tuples (immutable, hashable) with the clarity of named attributes.

What is a namedtuple?

A namedtuple is created using collections.namedtuple() and behaves like a tuple but with named fields.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)

print(p[0])    # 10 (positional access still works)
print(p.x)     # 10 (named access is clearer)
print(p.y)     # 20

Why use namedtuples?

Regular tuples are hard to read:

user = ("Alice", 30, "alice@example.com")
print(user[0])  # Is this name? Age? Email? Unclear.
print(user[1])
print(user[2])

Namedtuples are self-documenting:

User = namedtuple('User', ['name', 'age', 'email'])
user = User("Alice", 30, "alice@example.com")
print(user.name)   # Clear: this is the name
print(user.age)    # Clear: this is the age
print(user.email)  # Clear: this is the email

Creating namedtuples

Syntax 1: List of field names

Point = namedtuple('Point', ['x', 'y'])

Syntax 2: Space-separated string

Point = namedtuple('Point', 'x y')

Both are equivalent.

Using namedtuples

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

p1 = Point(0, 0)
p2 = Point(3, 4)

# Access by name
print(p1.x, p1.y)  # 0 0

# Tuple unpacking still works
x, y = p1
print(x, y)        # 0 0

# Still immutable
p1.x = 5  # AttributeError: can't set attribute

Namedtuples are still tuples

They retain all tuple benefits:

Still hashable (can be dict keys, set members):

cache = {
    Point(0, 0): "origin",
    Point(1, 2): "point A"
}

Still immutable:

p = Point(10, 20)
p[0] = 99  # TypeError: 'Point' object does not support item assignment

Can be unpacked:

x, y = Point(10, 20)

Real-world example: Coordinate system

Without namedtuple (hard to read):

points = [(0, 0), (1, 0), (1, 1), (0, 1)]

for point in points:
    print(f"x={point[0]}, y={point[1]}")  # What are [0] and [1]?

With namedtuple (self-documenting):

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
points = [Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)]

for point in points:
    print(f"x={point.x}, y={point.y}")  # Clear and readable

Real-world example: Function returns

Without namedtuple:

def get_user():
    return ("Alice", 30, "alice@example.com")

name, age, email = get_user()

It works, but ("Alice", 30, "alice@example.com") is cryptic. What does each field mean?

With namedtuple:

from collections import namedtuple

User = namedtuple('User', ['name', 'age', 'email'])

def get_user():
    return User("Alice", 30, "alice@example.com")

user = get_user()
print(user.name, user.age, user.email)  # Self-documenting

When to use namedtuples

Use namedtuples when:

  • You have a fixed-size structure with labeled fields (like a coordinate, a user record, a database row)
  • You want immutability and hashability (dict keys, set members)
  • You want readability (named fields instead of magic indexing)
  • You want something lightweight (namedtuples are just tuples with names)

Use regular tuples when:

  • You just need a simple lightweight return value and clarity is obvious from context
  • You need variable-length sequences (tuples can be any size, namedtuples are fixed)

Use dataclasses when:

  • You need mutable objects with named fields
  • You need more complex behavior (methods, defaults, validation)

1.3.11 Verbal interview questions

Answer these out loud:

  • Why use tuple instead of list?
  • Why can tuple be a dictionary key?
  • Explain unpacking
  • Explain extended unpacking
  • How can a tuple still contain mutable data?
  • What is a namedtuple and why would you use it?

1.3.12 Coding drills

Drill 1: swap values

def swap(a, b):
    ...

Use tuple unpacking.

Drill 2: return min and max

def min_max(nums: list[int]) -> tuple[int, int]:
    ...

Return both values as a tuple.

Drill 3: coordinate map

points = {
    (0, 0): "origin",
    (1, 2): "point"
}

Explain why tuple works as key.


1.3.10 Common interview problems

Tuples appear frequently in coding interviews, especially in graph problems, coordinate problems, and when working with multiple return values.

Problem 1: Merge Intervals with Tuples

Question: Given a list of tuples representing intervals (start, end), merge overlapping intervals.

def merge(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]:
    # intervals = [(1, 3), (2, 6), (8, 10), (15, 18)]
    # Return [(1, 6), (8, 10), (15, 18)]
    ...

Visual: [(1, 3), (2, 6)] overlap → merge to [(1, 6)]

Key insight: Sort by start time, iterate and merge when overlapping.


Problem 2: Multiple Return Values

Question: A function needs to return min, max, and count from a list. Design this using tuples.

def stats(nums: list[int]) -> tuple[int, int, int]:
    # nums = [5, 1, 9, 3]
    # Return (1, 9, 4)  # min, max, count
    ...

Key insight: Return tuple, caller unpacks with min_val, max_val, count = stats(nums)


Problem 3: Graph Coordinates

Question: Given a 2D grid, find a path from start to end. Use tuples for coordinates.

def find_path(grid: list[list[int]], start: tuple[int, int], end: tuple[int, int]) -> list[tuple[int, int]]:
    # start = (0, 0), end = (3, 3)
    # Return [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (3, 2), (3, 3)]
    ...

Key insight: Use tuples as coordinate keys—can add to sets or use as dict keys to track visited nodes.

visited = set()
visited.add((0, 0))
if (0, 1) not in visited:
    ...

Problem 4: Sorting with Tuple Keys

Question: Sort a list of people by age, then by name (use tuple as sort key).

def sort_people(people: list[dict]) -> list[dict]:
    # people = [
    #     {"name": "Alice", "age": 30},
    #     {"name": "Bob", "age": 25},
    #     {"name": "Carol", "age": 30}
    # ]
    # Return sorted by age, then name
    ...

Key insight: Use tuple as sort key: sorted(people, key=lambda p: (p["age"], p["name"]))


Problem 5: Two Sum with Tuples

Question: Find two numbers in a list that sum to a target. Return as tuple (index1, index2).

def twoSum(nums: list[int], target: int) -> tuple[int, int]:
    # nums = [2, 7, 11, 15], target = 9
    # Return (0, 1)  # indices of 2 and 7
    ...

Key insight: Use dictionary to store value→index mapping, then check for complement.

num_map = {}
for i, num in enumerate(nums):
    complement = target - num
    if complement in num_map:
        return (num_map[complement], i)
    num_map[num] = i

Problem 6: Tuple as Dictionary Key

Question: Given a matrix of cities and distances, use tuples as dictionary keys to store distances efficiently.

def build_distance_map(cities: list[str], distances: list[tuple[str, str, int]]) -> dict:
    # distances = [("NYC", "LA", 2800), ("LA", "NYC", 2800), ...]
    # Return {("NYC", "LA"): 2800, ...}
    ...

Key insight: Tuples as keys because they're hashable and immutable.

dist_map = {}
for city1, city2, distance in distances:
    dist_map[(city1, city2)] = distance

Problem 7: Unpacking Function Returns

Question: A function returns division result and remainder. Unpack these values.

def divmod_custom(a: int, b: int) -> tuple[int, int]:
    # a = 17, b = 5
    # Return (3, 2)  # quotient, remainder
    ...

q, r = divmod_custom(17, 5)  # Unpacking

Key insight: Tuple return → destructuring assignment at call site.


Problem 8: Extended Unpacking

Question: Given a tuple of coordinates, extract first point, middle points, and last point.

def analyze_path(path: tuple[tuple[int, int], ...]) -> tuple:
    # path = ((0, 0), (1, 1), (2, 2), (3, 3), (4, 4))
    # first, *middle, last = path
    # Return first, middle list, last
    ...

Key insight: Extended unpacking with *middle to capture variable-length sequences.


Problem 9: Swap Without Temporary Variable

Question: Swap two variables using tuple unpacking.

def swap(a: int, b: int) -> tuple[int, int]:
    a, b = b, a
    return (a, b)

Why it works: Python creates a tuple (b, a), then unpacks to a, b.


Problem 10: Namedtuples for Clarity

Question: Instead of plain tuples, use namedtuple for better readability in a coordinate system.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

def distance(p1: Point, p2: Point) -> float:
    # p1 = Point(x=0, y=0), p2 = Point(x=3, y=4)
    # Return 5.0 (3-4-5 triangle)
    return ((p1.x - p2.x)**2 + (p1.y - p2.y)**2)**0.5

Key insight: namedtuple provides named fields instead of positional indexing. Still immutable and hashable, but more readable.