Lists
Why Lists & Tuples Matters
The Problem: Building collections by hand with indexes is fragile and error-prone, especially when sizes change.
The Solution: Python provides ergonomic mutable lists, immutable tuples for fixed records, and comprehensions / generators for declarative, lazy data transforms.
Real Impact: List comprehensions and generators are the single biggest productivity gain in everyday Python — they let you describe what you want instead of how to loop for it.
Real-World Analogy
Think of lists vs tuples like notebooks vs sealed envelopes:
- List = a notebook — you can scribble in, add pages, rip pages out
- Tuple = a sealed envelope — the contents are fixed, hashable, safe to use as a key
- Slice = tearing out a contiguous range of pages
- Comprehension = an assembly line that transforms each item as it flies past
- Generator = a printer that produces the next page only when you ask
Lists are mutable, ordered, heterogeneous sequences — Python's workhorse data structure.
nums = [1, 2, 3, 4, 5]
mixed = [1, "two", 3.0, [4, 5]] # any types, including nested
empty = [] # or list()
# Index and slice
nums[0] # 1
nums[-1] # 5 (last)
nums[1:4] # [2, 3, 4]
nums[::2] # [1, 3, 5] — every other
nums[::-1] # [5, 4, 3, 2, 1] — reversed
Modifying Lists
nums.append(6) # add to end — O(1) amortized
nums.extend([7, 8]) # add multiple
nums.insert(0, 0) # insert at index — O(n)
nums.pop() # remove and return last
nums.pop(0) # remove and return first — O(n)
nums.remove(3) # remove first occurrence of value
del nums[2] # delete by index
nums.clear() # empty the list
nums.reverse() # in place
nums.sort() # in place, ascending
nums.sort(reverse=True, key=abs)
sort() vs sorted()
list.sort() sorts in place and returns None. sorted(iterable) returns a new list and works on any iterable. Use sorted in pipelines.
Tuples
Tuples are immutable, ordered sequences. Use them for fixed-size records and as dict keys.
point = (3, 4)
rgb = (255, 128, 0)
singleton = (42,) # trailing comma — NOT (42)
empty = ()
# Tuple unpacking
x, y = point
a, b, c = rgb
first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4]
*init, last = [1, 2, 3, 4] # init=[1,2,3], last=4
# Tuples are hashable — can be dict keys
distances = {(0, 0): 0, (3, 4): 5}
NamedTuple — Tuples with Field Names
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(3.0, 4.0)
print(p.x, p.y) # field access
print(p[0]) # still indexable
When to use list vs tuple
Use a list when the size or contents will change. Use a tuple for fixed data — function returns, dict keys, records. Tuples are slightly faster and use less memory.
List Comprehensions
The Pythonic way to build a list from another iterable.
squares = [n * n for n in range(10)]
# With a filter
evens = [n for n in nums if n % 2 == 0]
# Conditional expression in the value
labels = ["big" if n > 100 else "small" for n in nums]
# Nested — flatten a 2D list
matrix = [[1, 2], [3, 4]]
flat = [n for row in matrix for n in row]
# Build a list of lists (matrix)
identity = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
⚠️ Don't [x] * n mutable objects
grid = [[0]*3]*3 creates 3 references to the same inner list — mutating one mutates all. Use a nested comprehension instead.
Generators and Generator Expressions
Generators produce values lazily — they don't build the whole list in memory. Essential for large or infinite sequences.
Generator Expressions
# Parens instead of brackets — lazy
total = sum(n * n for n in range(1_000_000))
# Memory: list comp builds 1M-element list; gen exp uses ~constant memory
Generator Functions with yield
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
The itertools Module
from itertools import chain, count, cycle, takewhile, groupby, accumulate
list(chain([1, 2], [3, 4])) # [1, 2, 3, 4]
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] — running totals
list(islice(count(10, 2), 5)) # [10, 12, 14, 16, 18]
Sorting
nums = [3, 1, 4, 1, 5, 9, 2, 6]
sorted(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
sorted(nums, reverse=True) # descending
# Sort by computed key
words = ["banana", "apple", "cherry"]
sorted(words, key=len) # shortest first
sorted(words, key=str.lower) # case-insensitive
# Sort list of dicts
users = [{"name": "Bob", "age": 30}, {"name": "Alice", "age": 25}]
sorted(users, key=lambda u: u["age"])
# operator.itemgetter is faster + clearer for multi-key sort
from operator import itemgetter
sorted(users, key=itemgetter("age", "name"))
Stable sort
Python's sort is stable: equal elements retain their relative order. This means you can sort by multiple keys with successive sorts (least significant first).
🎯 Practice Exercises
Exercise 1: Chunk a list
Write chunk(lst, size) that returns a list of size-n sublists. E.g. chunk([1,2,3,4,5], 2) → [[1,2], [3,4], [5]].
Exercise 2: Rolling average
Given a list of numbers, return a list of 3-element rolling averages using a generator and itertools.
Exercise 3: Sort by two keys
Sort a list of (name, score, age) tuples by score descending, then age ascending.
Exercise 4: Lazy primes
Write a generator that yields prime numbers indefinitely. Use itertools.islice to get the first 20.