Lists and Tuples

Easy35 min read

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.