Lists, tuples, sets, dicts — and when to reach for each
Time complexity, memory trade-offs, and the collections module (deque, Counter, defaultdict).
Why this matters
Choosing the right built-in container is the difference between O(n²) and O(n). Fluent use of list, dict, set, tuple (plus deque, Counter, defaultdict from collections) is the most common signal of a senior Python developer. Comprehensions are how you express transformations Pythonically.
Learning objectives
- Pick the right container for the access pattern.
- Use slicing and indexing fluently.
- Write list, dict, set, generator comprehensions.
- Apply
collections.Counter,defaultdict,deque,namedtuple. - Reason about complexity of common operations.
1. The four core containers
| Type | Mutable | Ordered | Index by | Use when |
|---|---|---|---|---|
list | yes | yes | int | sequence of homogeneous items, frequent append |
tuple | no | yes | int | fixed record, hashable, returned from function |
dict | yes | yes (insertion order, 3.7+) | hashable key | key→value lookup |
set | yes | no | — | membership, dedup, set algebra |
Plus frozenset (immutable set) and bytes/bytearray for binary.
Complexity cheat sheet (CPython)
| Operation | list | dict | set |
|---|---|---|---|
| index / get | O(1) | O(1) avg | — |
| append / add | O(1) amortised | O(1) | O(1) |
| insert at start | O(n) | — | — |
x in container | O(n) | O(1) | O(1) |
| iterate | O(n) | O(n) | O(n) |
| sort | O(n log n) | — | — |
If in is hot, use a set/dict — not a list.
2. Lists
xs = [1, 2, 3, 4]
xs.append(5) # add to end
xs.extend([6, 7]) # bulk add
xs.insert(0, 0) # add at index (O(n))
xs.pop() # remove + return last (O(1))
xs.pop(0) # remove + return first (O(n))
xs.remove(3) # remove first occurrence by value
del xs[0]
xs.reverse() # in-place
xs.sort() # in-place sort
sorted(xs, reverse=True) # new list
xs.count(2), xs.index(2)
# Slicing
xs[1:4] # slice
xs[::-1] # reversed copy
xs[::2] # every other
xs[:] = [99] # replace all in placelist vs array vs numpy
list: heterogeneous; pointers to Python objects.array(stdlib): homogeneous, compact (array('i', [1,2,3])).numpy.ndarray: homogeneous, vectorised — the right choice for numeric data (Phase 5.1).
Sorting
sorted(records, key=lambda r: r["age"])
sorted(records, key=lambda r: (r["last_name"], r["first_name"]))
sorted(records, key=operator.itemgetter("age")) # faster
# Stable sort: items with equal keys keep relative order3. Tuples
Immutable, hashable (if contents are hashable), often used as records.
p = (1, 2, 3)
x, y, z = p # unpack
p[0]
len(p)
# Single-element tuple needs the trailing comma:
single = (1,) # tuple
not_tuple = (1) # int
# Returned from functions
def divmod_(a, b):
return a // b, a % b # implicit tuple
q, r = divmod_(7, 2)For named records, prefer NamedTuple or dataclass(frozen=True):
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
z: float = 0.0
p = Point(1, 2)
p.x, p.y, p[0] # both work4. Dicts
d = {"a": 1, "b": 2}
d["a"] # KeyError if missing
d.get("z", 0) # default
d.setdefault("a", 99) # only sets if missing
d.update({"c": 3})
# Iteration
for k in d: ...
for k, v in d.items(): ...
for v in d.values(): ...
# Construction
dict(a=1, b=2)
dict([("a", 1), ("b", 2)])
{x: x*x for x in range(5)} # comprehension
# Merge (3.9+)
e = {"d": 4}
merged = d | e # new dict
d |= e # in-place
# Membership: O(1) average
"a" in dKeys must be hashable
int,str,tupleof immutables — yes.list,dict,set— no (TypeError: unhashable type).- Custom classes — by default hash on
id(); override__hash__/__eq__(Phase 2.1).
collections upgrades
from collections import Counter, defaultdict, OrderedDict, ChainMap, deque
# Counter — frequency table
c = Counter("abracadabra")
c.most_common(2) # [('a', 5), ('b', 2)]
c["z"] # 0, not KeyError
# defaultdict — auto-create missing values
buckets = defaultdict(list)
for word in words:
buckets[len(word)].append(word)
# OrderedDict — rarely needed since 3.7 (regular dict is ordered)
# Use only for .move_to_end() and equality-with-order semantics
# ChainMap — layer dicts; lookup falls through
config = ChainMap(local_overrides, defaults)
# deque — O(1) appends/pops from BOTH ends
from collections import deque
q = deque(maxlen=10)
q.append(1); q.appendleft(0); q.pop(); q.popleft()5. Sets
Unordered, fast in, set algebra.
s = {1, 2, 3}
empty = set() # NOT {} — that's an empty dict
s.add(4); s.remove(1); s.discard(99) # discard doesn't raise
# Set operations
a = {1, 2, 3}
b = {2, 3, 4}
a | b # union {1,2,3,4}
a & b # intersection {2,3}
a - b # difference {1}
a ^ b # symmetric diff {1, 4}
a <= b # subset?
a.isdisjoint({99, 100})
# Comprehension
{x * 2 for x in range(5)}
# Frozen for keys / set elements
fs = frozenset([1, 2, 3])Use for: deduplication, fast in, set algebra, hashable-only collections.
6. Comprehensions
The single most distinctive Pythonic construct.
# List
squares = [x * x for x in range(10)]
even_squares = [x * x for x in range(10) if x % 2 == 0]
# With multiple loops (Cartesian product)
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]
# Dict
inverted = {v: k for k, v in d.items()}
# Set
unique_lengths = {len(w) for w in words}
# Nested
matrix = [[i * j for j in range(3)] for i in range(3)]Generator expressions
Same syntax with (...) instead of [...] — lazy:
gen = (x * x for x in range(10**9)) # doesn't materialise
sum(x * x for x in range(10**9)) # streams; constant memoryUse generator expressions when you only need to iterate once and the input is huge.
When to NOT use a comprehension
- Three or more
for/ifclauses — write a loop. - You want a side effect (
print,appendto external list) — write a loop. - Readability suffers.
7. Iteration helpers
from itertools import (
chain, zip_longest, accumulate, count, cycle, islice,
groupby, combinations, permutations, product, takewhile,
)
# Chain — flatten
list(chain([1, 2], [3, 4])) # [1, 2, 3, 4]
list(chain.from_iterable([[1,2],[3,4]]))
# Accumulate — running totals
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10]
list(accumulate([1, 2, 3, 4], func=max)) # [1, 2, 3, 4]
# Islice — slice a generator
list(islice(count(), 5)) # [0, 1, 2, 3, 4]
# Group consecutive equal items
for key, group in groupby("AAABBC"):
print(key, list(group)) # A AAA, B BB, C C
# All pairs / combinations
list(combinations(range(4), 2)) # [(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)]
list(product([1, 2], ["a", "b"])) # [(1,"a"),(1,"b"),(2,"a"),(2,"b")]itertools is the standard library's best gift. Memorise the names.
8. Worked example: counting words
from collections import Counter
from pathlib import Path
text = Path("alice.txt").read_text().lower()
words = (w.strip(".,!?;:'\"") for w in text.split())
c = Counter(words)
# Top 10 (ignoring trivial stopwords)
stop = {"the", "and", "a", "of", "to", "in", "is", "it"}
top = [(w, n) for w, n in c.most_common(50) if w not in stop][:10]
print(top)8 lines from "raw text" to "top words." Few languages match this conciseness.
9. Sorting deep cuts
records = [
{"name": "Ada", "score": 90},
{"name": "Bob", "score": 85},
{"name": "Cara", "score": 90},
]
# By score desc, then name asc
records.sort(key=lambda r: (-r["score"], r["name"]))
# Stable: items equal on key keep original order
sorted([3, 1, 2, 1], key=lambda x: x) # [1, 1, 2, 3] — first 1 came first
# Functools.cmp_to_key for legacy compare functions
from functools import cmp_to_key
def cmp(a, b):
return (a > b) - (a < b)
sorted([3, 1, 2], key=cmp_to_key(cmp))For huge data, NumPy / pandas sort is faster (compiled).
10. Choosing the right container — quick guide
| Problem | Use |
|---|---|
| Fixed-size record returned from function | tuple (or NamedTuple) |
| List of things, frequent append | list |
| Append/pop from both ends | deque |
| Lookup by name | dict |
| Membership test | set |
| Counting occurrences | Counter |
| Grouping by key | defaultdict(list) or itertools.groupby |
| Numerical homogeneous data | numpy.ndarray |
| Layered config (env > file > defaults) | ChainMap |
| LRU cache | functools.lru_cache or OrderedDict |
| Priority queue | heapq |
Hands-on lab (2 hours)
- Implement
unique_preserve_order(seq)returning a list of items without duplicates, keeping order. - Build a word-frequency histogram from a text file; print top 20.
- Group words by their first letter using
defaultdict(list). - Implement an LRU cache using
OrderedDict(move_to_end+popitem(last=False)). - Use
itertools.groupbyto find runs of identical chars in a string. - Sort a list of dicts by multiple keys with
operator.itemgetter. - Convert a list of records into a dict keyed by
id; benchmarkinlookup against the original list. - Bonus: solve "two-sum" (return indices that sum to target) using a single dict pass.
Common pitfalls
{}is a dict, not a set. Empty set isset().- Modifying a list while iterating: confusing skips. Iterate over a copy or build a new list.
- Using a list for membership test on big data.
- Comprehensions that build up huge lists when a generator would suffice.
d.keys()returns a view, not a list — fine for iteration; index it withlist(d.keys())[0]if needed (but that suggests a different data structure).
Self-check
- Complexity of
x in listvsx in set. - How to dedup while preserving order?
- What's the difference between a list and a tuple beyond mutability?
- Why is
OrderedDictrarely needed now? - State three uses of
itertools.
References
- Fluent Python, Ramalho — Chapters 2, 3, 7.
- Effective Python, Slatkin — Items 11-30.
- Python docs,
collections,itertools,bisect,heapq. - TimeComplexity page: https://wiki.python.org/moin/TimeComplexity.
Sign in to save your progress and earn badges.