Finding one customer in a list of a million records means checking rows until you hit it. Finding them in a dictionary means asking by name — instantly, whether there are a hundred entries or a hundred million. That lookup-by-name superpower is why dicts are everywhere: configs, JSON, API responses, pandas' internals.
What you'll learn
What
A dictionary maps KEYS to VALUES: {'name': 'Ada', 'plan': 'pro'}. You store by key, retrieve by key, and update by key — no positions, no scanning. Keys must be immutable (strings, numbers, tuples); values can be anything.
Why
Data IS key→value at every scale: a user profile, a config file, a JSON API response, a row with named columns, a count per category. Fluency with dicts — including .get(), .items(), and the counting pattern — is the single highest-leverage Python skill for data work.
Where it's used
JSON payloads, configuration, counting/grouping (the heart of analytics), lookup tables, function kwargs, and pandas' column-oriented core.
Where this runs in production
A Stripe charge arrives as JSON — in Python, a dict: charge['amount'], charge['currency'], charge['metadata']['order_id']. Payment integrations are dict navigation.
Service configuration ships as nested dicts — flags, limits, rollout percentages — read by key at startup. A typo'd key raising KeyError loudly beats a silent wrong default.
Vocabulary building is the counting pattern: one pass over tokens, counts[word] = counts.get(word, 0) + 1 — the exact loop you'll write today, industrialized.
user = {'name': 'Ada', 'logins': 42}
print(user['name']) # read by key
user['plan'] = 'pro' # insert
user['logins'] += 1 # update in place
print(user)
print('plan' in user) # membership tests KEYSThe bracket syntax looks like list indexing, but the meaning is different: user['name'] is a lookup by NAME, not position. Dicts preserve insertion order (guaranteed since Python 3.7), but you should think in keys, not positions.
A dictionary is a coat check. You hand over a coat (value) and get a numbered ticket (key). Retrieval is instant: present ticket 47, receive that exact coat — the attendant doesn't rummage through every rack (that's the list-scan you're escaping). Two coats can be identical, but tickets are unique: hand in a second coat under ticket 47 and it REPLACES the first. And a made-up ticket gets you an apologetic shrug — KeyError.
user['age'] on an absent key raises KeyError — loud, immediate, often exactly right (a missing config key SHOULD crash at startup). When absence is normal, choose deliberately: `if 'age' in user:` to test first, or user.get('age') for None, or user.get('age', 0) for a fallback. The counting pattern is built on that third form. Rule of thumb: [] when the key MUST exist, .get() when it may not.
votes = ['py', 'sql', 'py', 'r', 'py', 'sql']
counts = {}
for lang in votes:
counts[lang] = counts.get(lang, 0) + 1
print(counts)THE canonical dict pattern: count occurrences in one pass. .get(lang, 0) reads the current tally or 0 for first-timers; the assignment writes it back plus one. Every groupby you'll ever run is this loop wearing a suit.
Every JSON object you'll ever receive becomes a Python dict (json.loads) and back (json.dumps) — master dicts and you've mastered API data. Two edges to respect: (1) assigning to an existing key silently OVERWRITES — building a lookup from data with duplicate keys keeps only the last one, a classic silent data loss; (2) `for k in d: del d[k]` — mutating a dict while iterating it — raises RuntimeError; collect keys first or build a new dict.
One lookup, two possible outcomes, three policies for the miss. Click each node.
A product record with named fields.
Read by key name — no remembering that price is 'position 1'. Names beat positions for anything with more than two fields.
Your task
Run a small warehouse ledger. Starting from an empty dict, process a list of ('item', qty) movements (negative = shipped out). Build the stock levels with .get(), then print each item and its final level, plus a LOW STOCK warning list for anything below 5.
bolt: 10 nut: 3 washer: 3 LOW STOCK: ['nut', 'washer']
Write your solution in the editor on the right, then hit Run.
d = {'a': 1}; print(d['b']) — what happens?
Which CANNOT be a dictionary key?
counts = {}; then for each word: counts[word] = counts.get(word, 0) + 1. What does .get(word, 0) do for a word seen before?
What most likely happened?
grades = {'mia': 88, 'kai': 61, 'ada': 95}. Print each student as '<name>: <grade>' (insertion order), then print the name of the top scorer using max with a key. Expected: mia: 88 kai: 61 ada: 95 top: ada
Why is looking up one key in a dict of 10 million entries about as fast as in a dict of 10?
orders = [('mia', 'espresso'), ('kai', 'latte'), ('mia', 'latte'), ('ada', 'espresso'), ('mia', 'mocha')]. Group into a dict of customer → list of drinks, then print each customer as '<name> ordered <n>: <drinks joined by, >'. Expected: mia ordered 3: espresso, latte, mocha kai ordered 1: latte ada ordered 1: espresso
Why are dictionary lookups so fast, and what constraint on keys does that speed impose?
Dicts are hash tables: inserting a key hashes it — computes an integer from its value — which determines the bucket where the entry is stored. Lookup repeats the hash and jumps straight to that bucket, so the cost is essentially constant whether the dict holds ten entries or ten million, versus a list scan that grows linearly. The constraint follows directly: keys must be HASHABLE, meaning their hash can never change, which effectively means immutable — strings, numbers, tuples of immutables. A mutable key like a list would leave its entry stranded in a stale bucket the moment it changed, so Python rejects it up front with TypeError: unhashable. That's also the practical answer to 'why tuples as composite keys': ('store-12', '2026-07-14') is frozen, so it hashes reliably forever.
Compare the three ways to handle a possibly-missing key. When is each right?
Bracket access — d[k] — raises KeyError on a miss: right when the key is REQUIRED, like a config value or schema field, because failing loudly at the source beats propagating a silent default into downstream computations. .get(k) returns None quietly: right when absence is a normal state and the caller will branch on it, tested with `is not None` when falsy values are legitimate. .get(k, default) returns a fallback: right for accumulation and display — counts.get(word, 0) + 1 is the canonical counting pattern, initializing first-timers and incrementing veterans in one expression. The interview-grade point: this is a POLICY decision about your data, made explicit in syntax — reaching for .get everywhere 'to be safe' actually hides real bugs that brackets would have caught on day one.
Sketch how you'd group a list of records by a field using plain dicts, and relate it to groupby.
One pass with a dict of lists: create an empty dict; for each record, extract the group key; if the key isn't present, initialize it with an empty list (or use setdefault(key, []) to do both steps at once); append the record. Afterwards, iterate .items() and reduce each bucket — sum, mean, count — into your result. That two-phase shape, partition-then-aggregate, is exactly what df.groupby('region')['amount'].mean() performs: pandas partitions rows into per-key buckets and applies the reduction per bucket, just vectorized and optimized. Knowing the manual version matters for three reasons: it works when data isn't tabular (nested JSON, streaming events), it demystifies groupby's semantics — including why the group key becomes the index — and it's a standard interview warm-up where the counting variant (values as ints) and grouping variant (values as lists) show you understand that a dict's value can be any accumulator you choose.
Common Mistakes to Avoid
1) d[key] on optional data — KeyError; choose your missing-key policy deliberately. 2) Building lookups from data with duplicate keys — later rows silently overwrite earlier ones; count both sides. 3) Using a list as a key — unhashable TypeError; a tuple works. 4) Mutating a dict while iterating it — RuntimeError; iterate a copy of the keys or build a new dict. 5) Expecting `in` to test values — it tests KEYS; use `in d.values()` explicitly. 6) .get() everywhere 'for safety' — required keys deserve the loud failure of brackets.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Quiz me: bracket, .get(), or in — which policy fits each of these six situations?' • 'Walk me through the counting pattern on a fresh dataset.' • 'Show a nested JSON payload and drill me on extracting fields.' • 'Explain hashing with a diagram-in-words.' • 'Interview mode: ask me to group records by a field and grade my code.'
Glossary
Dictionary (dict) — a mutable mapping of unique keys to values. Key — the immutable lookup handle; hashed for instant access. Value — the data stored under a key; any type. KeyError — raised by d[k] when k is absent. .get(k, default) — lookup with a fallback, never raises. .items()/.keys()/.values() — iteration views. Hashable — has a stable hash; required for keys. Counting pattern — d[k] = d.get(k, 0) + 1. Grouping pattern — dict of lists, append per key. setdefault(k, v) — get k's value, inserting v first if absent. JSON — the text format whose objects map to dicts.
Recommended Resources
• Docs: 'Dictionaries' in the official Python tutorial and the dict methods reference. • Read: collections.Counter docs — the standard library's professional counting pattern (Counter(votes).most_common(3)). • Practice: take any API's example JSON response and extract three nested fields, choosing bracket vs .get() deliberately for each. • Next in DSM: dicts give you unique KEYS — Sets take just that uniqueness and make it a structure of its own: membership, dedup, and set algebra.
Recap
✓ Dicts map immutable keys to any values; brackets read/insert/overwrite by key. ✓ Lookup is hash-powered: constant-time at any size — think in keys, not positions. ✓ Missing keys are a policy: [] loud, .get() quiet, .get(k, default) self-healing. ✓ The counting pattern (get(k, 0) + 1) and grouping pattern (dict of lists) power all analytics. ✓ .items() + unpacking is how dict loops are written. ✓ JSON objects ARE dicts in Python — API work is dict navigation. Next up: Sets. Strip a dict down to just its unique keys and you get the set — instant membership tests, one-line deduplication, and the union/intersection algebra of comparing groups.
Run your code to see the output here.