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:
- Test with normal input
- Test edge cases (empty sequences, single elements)
- Test that functions don't modify input when they shouldn't
- 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.