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.