Runs in your browser — no installs needed

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.

10
Lessons
25+
Code examples
0
Dependencies
Runs allowed

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 Run or hit Ctrl+Enter / Cmd+Enter to execute
  • Hit ↺ Reset to 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:

hello.py
Output
Click "Run" to execute
01 / 10

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, returns None if missing
  • d.get(key, default) — returns default if key missing
  • key in d — fast O(1) membership test
  • d.keys(), d.values(), d.items() — view objects
dict_basics.py
Output
Click "Run" to execute

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+), b wins on conflicts
  • {k: v for k, v in ...} — dict comprehension
  • d.setdefault(key, []) — insert default only if missing
  • d.pop(key, default) — remove and return value
dict_advanced.py
Output
Click "Run" to execute

Nested dictionaries

Dicts can hold any value — including other dicts. Use setdefault or carefully structured comprehensions to build nested structures without KeyError surprises.

nested_dict.py
Output
Click "Run" to execute
02 / 10

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 return 0
  • defaultdict(set) — missing keys return an empty set()
  • 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.

defaultdict_groups.py
Output
Click "Run" to execute

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.

graph_adjacency.py
Output
Click "Run" to execute
03 / 10

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 position
  • od.move_to_end(key, last=False) — move to first position
  • od.popitem(last=True) — remove & return last item (LIFO)
  • od.popitem(last=False) — remove & return first item (FIFO)
ordereddict_ops.py
Output
Click "Run" to execute

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.

lru_cache.py
Output
Click "Run" to execute
04 / 10

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 call
  • c.most_common(n) — top n elements by count
  • c.elements() — iterator over elements with repetition
  • c + d, c - d, c & d, c | d — arithmetic ops
counter_basics.py
Output
Click "Run" to execute

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).

counter_inventory.py
Output
Click "Run" to execute
05 / 10

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.

task_scheduler.py
Output
Click "Run" to execute

Merging sorted iterables

heapq.merge() combines multiple sorted iterables into a single sorted stream without loading everything into memory — ideal for large data sets.

heapq_merge.py
Output
Click "Run" to execute
06 / 10

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 ≥ x
  • bisect.bisect_right(a, x) — index of first item > x
  • bisect.insort(a, x) — insert x keeping sorted order
  • Use the returned index to search efficiently: a[i] == x
bisect_grades.py
Output
Click "Run" to execute

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.

bisect_insort.py
Output
Click "Run" to execute
07 / 10

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/left
  • dq.pop() / dq.popleft() — remove from right/left
  • dq.rotate(n) — rotate right by n (negative = left)
  • deque(maxlen=n) — auto-evicts oldest item on overflow
browser_history.py
Output
Click "Run" to execute

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.

sliding_window.py
Output
Click "Run" to execute
08 / 10

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 places
  • f"{x:,}" — thousands separator
  • f"{x:>10}" / f"{x:<10}" / f"{x:^10}" — alignment
  • f"{x=}" — debug: prints x=value (Python 3.8+)
receipt.py
Output
Click "Run" to execute

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}".

fstring_debug.py
Output
Click "Run" to execute
09 / 10

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
comprehensions.py
Output
Click "Run" to execute

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)

nested_comprehensions.py
Output
Click "Run" to execute
10 / 10

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
dataclass_basic.py
Output
Click "Run" to execute

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__.

dataclass_advanced.py
Output
Click "Run" to execute

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.

inventory.py
Output
Click "Run" to execute