Master Python's
Essential Data Structures
Ten focused, hands-on lessons covering the standard library features that separate good Python from great Python. Every concept comes with live, editable code you can run instantly.
How to use this tutorial
Each lesson explains a concept with clear prose, then gives you a live code editor to experiment in. The code runs via Pyodide — a real CPython runtime compiled to WebAssembly — so it behaves exactly like Python on your machine.
- Edit any code in the editor — it's fully interactive
- Press
Runor hitCtrl+Enter/Cmd+Enterto execute - Hit
↺ Resetto restore the original code - Navigate with the sidebar or the Prev/Next buttons
Try it now
This playground is fully live. Edit the code and hit Run to verify Python is working:
dict — Python Dictionaries
The dictionary is Python's most versatile built-in data structure — a hash map
storing key-value pairs with O(1) average-case lookup. Understanding dict
deeply unlocks almost every other data structure in this course.
Creating and accessing dictionaries
Dictionaries are created with curly braces { } or the dict()
constructor. Keys can be any hashable type — strings, numbers, tuples.
Access values with d[key] or the safer d.get(key, default)
which returns None (or your default) instead of raising a KeyError.
d.get(key)— safe access, returnsNoneif missingd.get(key, default)— returnsdefaultif key missingkey in d— fast O(1) membership testd.keys(),d.values(),d.items()— view objects
Advanced dict features
Modern Python (3.9+) supports the merge operator | to combine
dictionaries. Dict comprehensions let you transform data concisely.
setdefault() inserts a key only if it doesn't already exist,
making it ideal for initializing nested structures.
a | b— merge dicts (3.9+),bwins on conflicts{k: v for k, v in ...}— dict comprehensiond.setdefault(key, [])— insert default only if missingd.pop(key, default)— remove and return value
Nested dictionaries
Dicts can hold any value — including other dicts. Use setdefault
or carefully structured comprehensions to build nested structures without
KeyError surprises.
defaultdict — Auto-Initializing Dicts
collections.defaultdict is a dict subclass that calls a factory
function to supply missing values automatically — eliminating the boilerplate
of setdefault and key in d checks.
The problem with regular dicts
When building a dict that maps keys to lists or counts, a regular dict forces
you to check if the key exists before appending or incrementing. This leads to
repetitive if key not in d: d[key] = [] patterns. defaultdict
eliminates all of that by automatically creating a default value on first access.
defaultdict(list)— missing keys return an empty[]defaultdict(int)— missing keys return0defaultdict(set)— missing keys return an emptyset()- The factory can be any zero-argument callable, including lambdas
Tip: defaultdict is a drop-in replacement for dict — all regular dict methods work the same way.
Graph adjacency list with defaultdict
One of the most common uses of defaultdict(list) is building
graph representations. Each node maps to a list of its neighbors, and
adding edges requires no initialization code.
OrderedDict — Ordered Dictionaries
collections.OrderedDict remembers insertion order (like all
modern Python dicts) but adds extra powers: move_to_end(),
order-sensitive equality, and popitem from either end — ideal for LRU caches.
When to use OrderedDict over dict
Since Python 3.7, regular dict preserves insertion order too.
So why use OrderedDict? Two reasons: move_to_end()
lets you reorder keys in O(1), and two OrderedDicts compare
as unequal if their keys appear in different order — handy for history-aware data.
od.move_to_end(key)— move to last positionod.move_to_end(key, last=False)— move to first positionod.popitem(last=True)— remove & return last item (LIFO)od.popitem(last=False)— remove & return first item (FIFO)
LRU Cache implementation
A Least Recently Used (LRU) cache evicts the oldest-accessed item when
capacity is full. OrderedDict.move_to_end() makes this elegant:
on every access, move the item to the back; on eviction, pop from the front.
Tip: Python's functools.lru_cache does this automatically for functions. Use OrderedDict when you need a manual, size-bounded cache structure.
Counter — Counting Made Easy
collections.Counter is a dict subclass designed for tallying
hashable objects. It includes powerful methods for finding top items,
combining counts, and set-like operations between tallies.
Creating and using Counter
Pass any iterable — string, list, tuple — to Counter() and it
counts element occurrences automatically. Missing keys return 0
(not a KeyError). Counters support arithmetic: adding two
counters sums their counts; subtracting removes or zeros out.
Counter(iterable)— count elements in one callc.most_common(n)— topnelements by countc.elements()— iterator over elements with repetitionc + d,c - d,c & d,c | d— arithmetic ops
Counter arithmetic and inventory tracking
Counter arithmetic is useful for inventory systems, shopping carts, and
any scenario where you combine or subtract tallies. The &
operator finds the intersection (minimum counts), and | finds
the union (maximum counts).
heapq — Priority Queues
Python's heapq module implements a min-heap on top of a regular
list, giving you O(log n) push and pop with the smallest element always at
index 0. The secret to priority queues, Dijkstra's algorithm, and efficient top-K problems.
Min-heap fundamentals
A heap is a complete binary tree where every parent is smaller than its
children. Python's heapq stores this as a flat list.
Always call heapq.heapify() to convert an existing list,
and use heappush/heappop to maintain the heap property.
heapq.heappush(heap, item)— add item, O(log n)heapq.heappop(heap)— remove & return smallest, O(log n)heapq.heapify(list)— convert list to heap in-place, O(n)heap[0]— peek at smallest without removing, O(1)
Max-heap trick: Python only has min-heap. For max-heap, negate your values: push -x and negate when popping.
Merging sorted iterables
heapq.merge() combines multiple sorted iterables into a single
sorted stream without loading everything into memory — ideal for large data sets.
bisect — Binary Search
The bisect module provides O(log n) binary search into already-sorted
lists, and efficient sorted insertion. Stop linear-scanning sorted data —
use bisect to find insertion points in microseconds.
bisect_left and bisect_right
Both functions return the insertion point for a value in a sorted list.
bisect_left returns the leftmost position (before any existing
equal elements); bisect_right returns the rightmost position (after them).
insort combines find and insert in one O(log n) + O(n) call.
bisect.bisect_left(a, x)— index of first item ≥ xbisect.bisect_right(a, x)— index of first item > xbisect.insort(a, x)— insert x keeping sorted order- Use the returned index to search efficiently:
a[i] == x
Maintaining a sorted list with insort
bisect.insort() keeps a list sorted as you add items —
no need to sort again after each insertion. This is much faster than
repeated list.sort() calls when you're inserting items one by one.
deque — Double-Ended Queue
collections.deque is a thread-safe, O(1) append and pop from
both ends. Unlike lists, prepending to a deque does not shift the
entire array — making it the right choice for queues, sliding windows, and history buffers.
deque operations
Think of a deque as a double-ended tape. You can add or remove from
either end in constant time. The maxlen parameter caps the
size — new items push old ones off the opposite end automatically, which
is perfect for rolling windows and bounded history.
dq.append(x)/dq.appendleft(x)— add to right/leftdq.pop()/dq.popleft()— remove from right/leftdq.rotate(n)— rotate right by n (negative = left)deque(maxlen=n)— auto-evicts oldest item on overflow
Sliding window with maxlen
A deque(maxlen=k) automatically maintains a rolling window of
the last k items. As you append new items, the oldest are
automatically evicted from the left — no manual slice management needed.
f-strings — Modern String Formatting
F-strings (formatted string literals, introduced in Python 3.6) are the
fastest, most readable way to embed expressions in strings. They support
inline expressions, format specs, alignment, and even self-documenting
debug output with the = suffix.
Expressions and format specs
Any valid Python expression can go inside the curly braces of an f-string.
After the expression, add a colon and a format spec: f"{value:.2f}"
for two decimal places, f"{value:>10}" for right-aligned in
10 characters, f"{value:,}" for thousands separator.
f"{x:.2f}"— float with 2 decimal placesf"{x:,}"— thousands separatorf"{x:>10}"/f"{x:<10}"/f"{x:^10}"— alignmentf"{x=}"— debug: printsx=value(Python 3.8+)
Debugging with = and number formatting
Python 3.8 added the = specifier: f"{x=}" expands to
x=42 — the variable name and its value — which is extremely useful
for debugging without adding extra print arguments. You can combine it with
format specs: f"{x=:.2f}".
Comprehensions — Pythonic Iteration
Comprehensions are concise, readable syntax for building collections by transforming and filtering iterables. Python supports list, dict, set, and generator comprehensions — each unlocking a different pattern for writing expressive, fast, idiomatic Python.
List, dict, and set comprehensions
The formula is always: [expression for item in iterable if condition].
The if condition part is optional. Dict comprehensions use
{key: value for ...}; set comprehensions use {value for ...}.
[x**2 for x in range(10)]— list of squares{k: v for k, v in d.items() if v > 0}— filter a dict{word.lower() for word in text.split()}— unique lowercase words- Comprehensions are 20–50% faster than equivalent
for-loop +append
Nested comprehensions and generators
Comprehensions can be nested for matrix operations or flattening structures.
Generator expressions use () instead of [] and
are lazy — they compute one item at a time, using almost no memory. Use
generators when you only iterate once and don't need the full list in memory.
Tip: Pass a generator expression directly to sum(), any(), all(), min(), max() — no brackets needed: sum(x**2 for x in data)
dataclass — Modern Data Containers
@dataclass (Python 3.7+) automatically generates __init__,
__repr__, and __eq__ from field annotations —
eliminating boilerplate for data-holding classes while adding optional
ordering, freezing (immutability), and post-init validation.
Basic dataclass usage
Annotate class-level variables with types and decorate with @dataclass.
Python generates __init__ with matching parameters automatically.
Use field(default_factory=list) for mutable defaults — never use
mutable defaults directly (same rule as regular function arguments).
@dataclass— generates__init__,__repr__,__eq__@dataclass(frozen=True)— immutable (hashable, usable as dict key)@dataclass(order=True)— generates<,>, etc.field(default_factory=list)— safe mutable default__post_init__— runs after__init__for validation
Frozen dataclasses, ordering, and __post_init__
frozen=True makes instances immutable and hashable — you can
use them as dictionary keys or in sets. order=True generates
comparison methods based on field order. __post_init__ runs
validation or derived-field computation right after __init__.
Inventory system — putting it all together
A real-world example combining dataclass with field(default_factory),
__post_init__ validation, and methods for a complete inventory management system.