DSM
70 XP
30 minsIntermediate70 XP

Choosing the Right Structure

You now own four containers — and the most common code-review comment in data engineering is still 'why is this a list?' Every structure answers a QUESTION: by position? by name? is it present? in what order? Choose by the question and code writes itself; choose by habit and you fight your own data all day.

What you'll learn

  • Apply the four questions that select a structure
  • Predict lookup/membership costs for lists vs dicts/sets
  • Compose structures: dicts of lists, sets for indexes, tuples as keys
  • Refactor code that fights its structure into code that fits it
  • Defend structure choices in review and interviews

What

A decision framework for lists, tuples, dicts, and sets: what each structure optimizes for, what each costs, how the choice changes at scale, and how the pieces compose into real designs.

Why

The wrong structure isn't just slow — it breeds bugs (duplicate keys silently overwritten, order relied on where none exists) and unreadable code (index gymnastics where names belonged). Structure choice is the first design decision of every script, and interviews probe it directly.

Where it's used

Every function signature you design, every 'how should I hold this data?' moment, code review, and the system-design portion of data interviews.

Where this runs in production

InstagramFeed assembly under 100ms

Candidate posts arrive as lists, dedupe against seen-post SETS, hydrate via id→post DICTS, and ship ordered lists — all four structures in one request path, each doing its one job.

PalantirEntity resolution

Matching millions of records means composite tuple keys — (name_normalized, dob) — indexing candidate dicts, with sets tracking already-merged IDs. Structure choice IS the algorithm.

DuckDBWhy columns win

Analytical databases store data column-oriented for the same reason your dict-of-lists made stats one-liners: the structure matches the question ('aggregate this field'), and performance follows.

Theory

The core ideas, in plain language.

Key Concept
The four questions

1) How do I LOOK UP an item — by position (list), by name/key (dict), or 'is it present?' (set)? 2) Does ORDER carry meaning — sequence matters (list/tuple) or not (dict for lookup, set for membership)? 3) Can it CHANGE — accumulating (list/dict/set) or a fixed-shape fact (tuple)? 4) Are DUPLICATES meaningful — yes (list) or must values be unique (set, dict keys)? Answer these four and the structure is chosen for you.

Performance in one paragraph: lists index instantly by position but SEARCH by scanning — `x in big_list` and .index() walk elements one by one. Dicts and sets hash: lookup, membership, insert, delete are constant-time at any size. Appending to a list is fast; inserting at the front shifts everything. These few facts predict almost every 'why is my script slow?' answer at this level.
Analogy: The kitchen test

A restaurant kitchen holds the same ingredients four ways. The ticket rail is a LIST — order matters, duplicates fine, work the sequence. The labeled spice rack is a DICT — grab 'cumin' by name, no searching. The allergen board is a SET — one question only: 'is peanut in this dish?' The plated dish spec is a TUPLE — (protein, side, sauce), fixed shape, sealed. Ask a kitchen to find cumin in an unlabeled pile (list-as-lookup) and service melts down — same ingredients, wrong container.

# Same data, three questions, three structures
events = [('ada', 'login'), ('kai', 'login'), ('ada', 'purchase')]

sequence = [e for _, e in events]          # list: what happened, in order?
by_user = {}                                # dict of lists: what did EACH user do?
for user, action in events:
    by_user.setdefault(user, []).append(action)
active = {u for u, _ in events}             # set: WHO was active at all?
print(sequence, by_user, active, sep='\n')

One event stream, three legitimate structures — because there are three different QUESTIONS. Data doesn't have one true shape; questions do. (setdefault: the one-line grouping idiom from Dictionaries.)

Key Concept
Composition is the real skill

Real designs stack structures, each answering its layer's question: a dict of lists (group by key, keep order within), a dict keyed by tuples (composite lookup: ('store-12', '2026-07-14') → sales), a set alongside a list (order preserved, membership fast — the dedup-preserving-order idiom), a list of dicts (records in sequence). Name the question per layer and compose accordingly.

Watch out
The smells of a wrong structure

You chose wrong if you see: .index() or `in` on a big list inside a loop (wants a dict/set); parallel lists kept in sync by index — names[i], ages[i] (wants a list of dicts or a dict); a dict whose keys are 0,1,2,... (wants a list); manually deduplicating with nested loops (wants a set); row[3] with a comment explaining what 3 means (wants a dict or namedtuple). None are syntax errors — all are design errors the structure was begging you to avoid.

Visual Learning

See the concept, then explore it.

The structure decision tree

Start at the top question; each answer narrows to a structure. Click nodes for the reasoning.

Worked Examples

Watch it built up, one line at a time.

Very EasyFour datasets, four verdicts

Apply the four questions rapid-fire.

Step 1 of 1

Ordered + duplicates fine → list. Fixed shape, sealed → tuple. Lookup by name → dict. Presence question → set. Each access line uses the structure's native strength.

Code
01# 1. This week's closing prices, in date order -> list
02prices = [101.2, 99.8, 103.5, 102.0, 104.1]
03# 2. One GPS fix -> tuple
04fix = (59.9139, 10.7522)
05# 3. Product name by SKU -> dict
06names = {'KB-201': 'Keyboard', 'MS-330': 'Mouse'}
07# 4. SKUs ever returned by a customer -> set
08returned = {'KB-201', 'HD-880'}
09print(prices[-1], fix[0], names['MS-330'], 'KB-201' in returned)
Output
104.1 59.9139 Mouse True
Practice Coding

Your turn — write the code.

Your task

Build a mini library checkout report. You get a member list (with duplicates from a messy export), a checkout feed of (member, title) tuples, and a reference-only set of titles that can't leave the building. Produce: the distinct member count, per-member checkout lists (skipping and counting reference-title attempts), and the busiest member.

Expected output
members: 3
{'ada': ['Python Crash', 'Stats Done Wrong'], 'mia': ['Python Crash']}
blocked: 2
busiest: ada

Write your solution in the editor on the right, then hit Run.

Exercises

Prove it. Reach 80% to complete the lesson.

Mastery Gate0% / 80% required
Easy0/2 solved

You need product details by SKU, millions of times per day. Which structure holds the catalog?

Which situation genuinely wants a tuple?

Medium0/3 solved

`if user_id in seen_ids:` runs inside a loop over 1M events. seen_ids grows to 200k entries. Which type for seen_ids?

ScenarioA script maintains temperatures = {0: 21.5, 1: 23.0, 2: 19.8} — a dict whose keys are consecutive integers starting at 0 — and iterates it with for i in range(len(temperatures)).

What's the reviewer's structural note?

visits = [('ada', '/home'), ('kai', '/pricing'), ('ada', '/docs'), ('mia', '/home'), ('ada', '/pricing')] — choose structures to print: unique visitor count, page-view counts per PAGE (sorted by page name), and ada's visit count. Expected: visitors: 3 /docs: 1 /home: 2 /pricing: 2 ada: 3

Set for uniqueness:Distinct visitors via a set (comprehension or set())
Dict for counting:Page views use the .get(k, 0) + 1 pattern, sorted for output
Hard0/2 solved

Sensor readings must be looked up by (station_id, hour) — both together. Which design is most direct?

ScenarioA pipeline stores 2M rows as a list of dicts and computes ONLY column stats: mean amount, max latency, count by status. Memory is tight and the loops feel slow. Nothing ever processes a whole row.

What restructuring does the access pattern suggest?

Complete 80% more exercises to unlock.
Interview Prep

How this shows up in real interviews.

Walk me through how you choose among list, tuple, dict, and set for a new piece of data.

Show model answer

I ask four questions. Access: by position or sequence → list/tuple; by key → dict; by presence → set. Order: if it carries meaning, only the sequences preserve a meaningful one. Mutability: accumulating or evolving → list/dict/set; a fixed-shape fact — coordinates, a returned pair — → tuple, which also buys hashability. Duplicates: meaningful → list (or a counting dict if I need frequencies); must be unique → set, or dict keys. Then I sanity-check against scale: any repeated search or membership test inside a loop pushes lists toward dicts/sets, because scans are linear and hashes are constant-time. And I let composition resolve conflicts — order AND fast membership means a list plus a shadow set; grouped sequences mean a dict of lists. The meta-answer interviewers want: the structure is chosen by the QUESTION the code asks of the data, not by what the data looks like.

A colleague's join enriches 500k orders by scanning a 200k-customer list per order. Explain the problem and the fix as you would in review.

Show model answer

The problem is complexity, not code style: for each of 500k orders, `next(c for c in customers if c['id'] == cid)` — or any scan — walks up to 200k records, so the worst case is on the order of 10^11 comparisons; the job's runtime is the PRODUCT of the two sizes. The fix is one line before the loop: build a hash index, by_id = {c['id']: c for c in customers} — one 200k pass — then each order does a constant-time by_id.get(cid). Total work drops to one pass over each dataset, roughly 700k operations. I'd also make the miss policy explicit: .get returns None, so decide loudly whether an unknown customer skips, defaults, or fails the batch. And I'd name the general principle for the codebase: any lookup repeated inside a loop deserves a dict/set index built once outside it — this is exactly what a database hash join or pandas merge does under the hood.

When would you reach beyond the four built-ins — and what does each upgrade replace?

Show model answer

When a pattern I'm hand-rolling has a named tool. Counting with d[k] = d.get(k, 0) + 1 → collections.Counter, which adds .most_common(n) for top-N reports. Grouping with setdefault(k, []).append(v) → defaultdict(list), removing the per-key initialization noise. Tuples whose positions I keep documenting in comments → namedtuple or a dataclass, giving fields names without losing immutability. Queues where I .pop(0) from a list — which shifts every remaining element — → deque with its constant-time popleft. And the big one for data work: columns of numbers in lists with Python-loop math → NumPy arrays and DataFrames, which are the column-oriented dict-of-lists idea with contiguous memory and vectorized operations. The pattern in every case: the built-ins teach the semantics; the upgrade is the same semantics with the boilerplate and the performance cliff removed — so knowing the four cold is what makes the upgrades legible.

Common Mistakes to Avoid

1) Lists as lookup tables — repeated `in`/.index() scans; build a dict/set index once. 2) Parallel lists synced by index — one sort and they shear; use records. 3) Dicts with keys 0,1,2,... — that's a list. 4) Sets where duplicates carried meaning — counts vanish silently. 5) Nested dicts when a tuple key states the composite fact directly. 6) Defaulting to whatever structure the data ARRIVED in — reshape to match the questions you'll ask, not the feed you received.

Ask the AI Tutor

Try these prompts in the AI Tutor panel: • 'Give me ten datasets and make me pick a structure for each, then grade me.' • 'Show a slow list-scan join and walk me through indexing it.' • 'Quiz me on which composition fits: dict of lists, tuple keys, or set+list?' • 'When do I graduate to Counter, defaultdict, or namedtuple?' • 'Interview mode: ask me to design structures for a ride-sharing app's data and critique my choices.'

Glossary

Access pattern — how code will read the data (by position, key, or presence). Index (design sense) — a dict/set built to make lookups constant-time. Composite key — a tuple key encoding multiple dimensions. Linear scan — checking elements one by one; cost grows with size. Constant-time — cost independent of size (hash lookups). Composition — nesting structures so each layer answers its own question. Parallel lists — the aligned-by-index anti-pattern. Counter/defaultdict/namedtuple/deque — stdlib upgrades of the counting, grouping, record, and queue patterns. Column orientation — dict of lists; the DataFrame's conceptual shape.

Recommended Resources

• Docs: the collections module page — read Counter, defaultdict, namedtuple, deque with this lesson's patterns in mind. • Read: the 'Time Complexity' page on the Python wiki — the official costs behind this lesson's intuition. • Practice: pick any script you've written this course and audit every container against the four questions; refactor one mischoice. • Next in DSM: structures organize data — Classes & Objects (the OOP module) organize data WITH its behavior, starting with why a class is the natural home for state you've been threading through functions.

Recap

✓ Choose by question: position → list/tuple, name → dict, presence → set. ✓ Then refine: order meaningful? mutable? duplicates meaningful? fixed shape? ✓ Scans are linear, hashes are constant — repeated lookups demand an index. ✓ Compose: dicts of lists, tuple keys, set-beside-list; one question per layer. ✓ Know the smells: parallel lists, integer-keyed dicts, list-as-lookup, loop-dedup. ✓ Counter, defaultdict, namedtuple, deque, and DataFrames are these patterns, upgraded. Next up: Classes & Objects. Data Structures is complete — you can shape any data. The OOP module begins by bundling data WITH the functions that operate on it: your own types.

scratchpad — preview this lesson's challenge anytime
library_system.pyPython
Ready
Output

Run your code to see the output here.