Skip to main content

Lists and Tuples

In this chapter, we'll explore Python's core sequence types: lists and tuples. We'll learn:

  • Creating and modifying lists
  • Immutable tuples and when to use them
  • Slicing, indexing, and unpacking
  • The len() function and other sequence operations
  • Testing functions that work with sequences

Lists​

Lists are mutable, ordered collections. Let's build a function that works with lists.

Write the test first​

Create test_shapes.py:

from shapes import perimeter


def test_perimeter_rectangle():
rectangle = [10.0, 10.0, 10.0, 10.0]
got = perimeter(rectangle)
want = 40.0
assert got == want

Make it compile​

Create shapes.py:

def perimeter(sides):
pass

Make it pass​

def perimeter(sides):
"""Calculate the perimeter by summing all sides."""
return sum(sides)

Run the test - it passes!

Working with lists​

Lists support many operations:

# Creating lists
numbers = [1, 2, 3, 4, 5]
empty = []
mixed = [1, "hello", 3.14, True]

# Accessing elements (0-indexed)
first = numbers[0] # 1
last = numbers[-1] # 5 (negative indexes count from end)

# Modifying elements
numbers[0] = 10 # [10, 2, 3, 4, 5]

# Adding elements
numbers.append(6) # [10, 2, 3, 4, 5, 6]
numbers.insert(0, 0) # [0, 10, 2, 3, 4, 5, 6]

# Removing elements
numbers.pop() # Removes and returns last element
numbers.remove(10) # Removes first occurrence of 10

# Length
length = len(numbers)

Slicing​

Slicing extracts portions of a sequence:

numbers = [0, 1, 2, 3, 4, 5]

numbers[1:4] # [1, 2, 3] - from index 1 up to (not including) 4
numbers[:3] # [0, 1, 2] - from start to index 3
numbers[3:] # [3, 4, 5] - from index 3 to end
numbers[-2:] # [4, 5] - last 2 elements
numbers[::2] # [0, 2, 4] - every second element
numbers[::-1] # [5, 4, 3, 2, 1, 0] - reversed

Testing slicing functions​

def test_first_n():
numbers = [1, 2, 3, 4, 5]
got = first_n(numbers, 3)
want = [1, 2, 3]
assert got == want


def test_last_n():
numbers = [1, 2, 3, 4, 5]
got = last_n(numbers, 2)
want = [4, 5]
assert got == want

Implementation:

def first_n(items, n):
"""Return the first n items."""
return items[:n]


def last_n(items, n):
"""Return the last n items."""
return items[-n:] if n > 0 else []

Note the edge case handling - items[-0:] would return the whole list, so we check for n > 0.

Tuples​

Tuples are like lists, but immutable - they can't be changed after creation:

# Creating tuples
point = (3, 4)
single = (42,) # Single-element tuple needs trailing comma
empty = ()
also_tuple = 1, 2, 3 # Parentheses are optional

# Accessing elements (same as lists)
x = point[0] # 3
y = point[1] # 4

# This would cause an error:
# point[0] = 5 # TypeError: 'tuple' object does not support item assignment

When to use tuples vs lists​

Use tuples when:

  • Data shouldn't change (coordinates, RGB colors)
  • Returning multiple values from a function
  • Dictionary keys (lists can't be keys)
  • Data integrity is important

Use lists when:

  • Data needs to be modified
  • You're collecting items dynamically
  • Order matters and items may be added/removed

Unpacking​

Both lists and tuples support unpacking:

# Tuple unpacking
point = (3, 4)
x, y = point # x=3, y=4

# Works with lists too
first, second, third = [1, 2, 3]

# Extended unpacking with *
first, *rest = [1, 2, 3, 4, 5] # first=1, rest=[2, 3, 4, 5]
first, *middle, last = [1, 2, 3, 4, 5] # first=1, middle=[2, 3, 4], last=5

# Swapping values
a, b = b, a

Testing with unpacking​

Let's write a function that returns min and max:

def test_min_max():
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
min_val, max_val = min_max(numbers)
assert min_val == 1
assert max_val == 9

Implementation:

def min_max(numbers):
"""Return the minimum and maximum values as a tuple."""
return min(numbers), max(numbers)

The len function​

len() works with any sequence:

len([1, 2, 3]) # 3
len((1, 2, 3)) # 3
len("hello") # 5
len({1, 2, 3}) # 3 (sets too)
len({}) # 0

Testing length-based logic​

def test_is_empty():
assert is_empty([]) is True
assert is_empty([1]) is False
assert is_empty("") is True
assert is_empty("hello") is False

Implementation:

def is_empty(sequence):
"""Return True if the sequence is empty."""
return len(sequence) == 0

List methods​

Lists have many useful methods:

numbers = [3, 1, 4, 1, 5, 9, 2, 6]

# Sorting
numbers.sort() # Sorts in place: [1, 1, 2, 3, 4, 5, 6, 9]
sorted_copy = sorted(numbers) # Returns new sorted list

# Reversing
numbers.reverse() # Reverses in place
reversed_copy = list(reversed(numbers)) # Returns new reversed list

# Counting and finding
numbers.count(1) # 2 (two 1s in the list)
numbers.index(4) # Index of first 4

# Copying
shallow_copy = numbers.copy() # or numbers[:]

Testing sorting​

def test_sort_descending():
numbers = [3, 1, 4, 1, 5]
got = sort_descending(numbers)
want = [5, 4, 3, 1, 1]
assert got == want
# Original should be unchanged
assert numbers == [3, 1, 4, 1, 5]

Implementation:

def sort_descending(numbers):
"""Return a new list sorted in descending order."""
return sorted(numbers, reverse=True)

Nested lists​

Lists can contain other lists:

matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]

# Accessing elements
matrix[0] # [1, 2, 3]
matrix[0][1] # 2
matrix[1][2] # 6

Testing matrix operations​

def test_flatten():
matrix = [[1, 2], [3, 4], [5, 6]]
got = flatten(matrix)
want = [1, 2, 3, 4, 5, 6]
assert got == want

Implementation:

def flatten(matrix):
"""Flatten a 2D list into a 1D list."""
return [item for row in matrix for item in row]

This nested list comprehension is equivalent to:

def flatten(matrix):
result = []
for row in matrix:
for item in row:
result.append(item)
return result

Named tuples​

For tuples with named fields, use namedtuple:

from collections import namedtuple

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

p = Point(3, 4)
print(p.x) # 3
print(p.y) # 4

# Still works like a tuple
x, y = p
print(p[0]) # 3

Testing with named tuples​

from collections import namedtuple

Rectangle = namedtuple('Rectangle', ['width', 'height'])


def test_rectangle_area():
rect = Rectangle(width=10, height=5)
got = area(rect)
want = 50
assert got == want

Implementation:

def area(rectangle):
"""Calculate the area of a rectangle."""
return rectangle.width * rectangle.height

Common patterns​

Checking if an element exists​

if 5 in numbers:
print("Found 5!")

if "apple" not in fruits:
print("No apples")

Getting unique elements​

numbers = [1, 2, 2, 3, 3, 3]
unique = list(set(numbers)) # [1, 2, 3] (order may vary)

# Preserving order (Python 3.7+)
unique = list(dict.fromkeys(numbers)) # [1, 2, 3]

Zipping lists together​

names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

paired = list(zip(names, scores))
# [('Alice', 85), ('Bob', 92), ('Charlie', 78)]

Wrapping up​

We've covered:

  • Lists - Mutable sequences: [1, 2, 3]
  • Tuples - Immutable sequences: (1, 2, 3)
  • Indexing - Access elements with sequence[index]
  • Slicing - Extract portions with sequence[start:end:step]
  • Unpacking - Assign multiple values: a, b = (1, 2)
  • len() - Get the length of any sequence
  • List methods - append, sort, reverse, copy, etc.
  • Named tuples - Tuples with named fields

The TDD takeaway​

When testing functions that work with sequences:

  1. Test with normal input
  2. Test edge cases (empty sequences, single elements)
  3. Test that functions don't modify input when they shouldn't
  4. Use meaningful test names that describe the scenario

Remember: tuples are your friend when data shouldn't change. Lists are great when you need flexibility.