Python dict cheatsheet for analysts
Contents:
Why dict is the analyst's workhorse
If you spend your days pulling SQL into pandas and reshaping the result, you already lean on Python dictionaries more than you realize. Code-to-label mappings, config blobs for query parameters, and counters for event streams are all dicts under the hood. When an interviewer at Stripe or DoorDash hands you a one-screen Python question, dict trivia is the single most common topic after list comprehensions.
The trick is that dict has two personalities. It's a constant-time lookup table when you treat it as {key: value}, and it's a flexible struct when you treat it as a small bag of named fields. Most bugs come from confusing the two — you reach for [] on a struct field that is sometimes missing, and your notebook explodes on the third row of a 50k-row dataframe.
This guide is deliberately analyst-flavored. You will not see object-oriented gymnastics or __hash__ overrides. You will see the patterns that survive code review at companies that ship dashboards weekly.
Creating a dict
There are five ways to build a dict, and you should know all of them by muscle memory. The literal {...} is what you reach for 90% of the time. dict(name=..., age=...) is convenient when keys are valid Python identifiers. dict(pairs) is what you get back from zip(keys, values) or from pandas' df.to_dict('records') rows. dict.fromkeys(...) initializes many keys to the same value — useful for counters before you graduate to Counter. And {} is the empty literal.
# Literal — the workhorse
user = {'name': 'Anna', 'age': 25, 'city': 'Berlin'}
# Constructor with kwargs
user = dict(name='Anna', age=25, city='Berlin')
# From pairs (zip, df rows, etc.)
pairs = [('name', 'Anna'), ('age', 25)]
user = dict(pairs)
# Same value for every key
metrics = dict.fromkeys(['dau', 'wau', 'mau'], 0)
# {'dau': 0, 'wau': 0, 'mau': 0}
empty = {}Hashability rule: keys must be hashable — strings, numbers, tuples of hashables, frozensets. Lists and other dicts cannot be keys. This is the most-asked dict question on data analyst interviews.
Access patterns: [] vs .get()
Brackets vs .get() is the dict question that separates juniors from people who have shipped production code. Both work. They fail differently.
config = {'host': 'localhost', 'port': 5432, 'db': 'analytics'}
# Brackets — raises KeyError if missing
config['host'] # 'localhost'
config['password'] # KeyError: 'password'
# .get() — returns None (or a default)
config.get('host') # 'localhost'
config.get('password') # None
config.get('password', 'secret') # 'secret'Use brackets when the key must exist — you want a loud, immediate crash that points to a contract violation upstream. Use .get() with an explicit default when you are reading optional fields out of an API payload, a JSON blob, or a CSV row that has been seen to vary.
| Method | Missing key behavior | When to use |
|---|---|---|
d[key] |
Raises KeyError |
Required field, contract-level |
d.get(key) |
Returns None |
Optional field, downstream handles None |
d.get(key, default) |
Returns default |
Optional field with a sane fallback |
d.setdefault(key, default) |
Sets and returns default |
Lazy init of nested containers |
The setdefault pattern shows up in legacy code; defaultdict is almost always cleaner.
Update, merge, delete
Updating a dict is one assignment. Merging two dicts is one operator since Python 3.9. Deleting has three flavors depending on whether you want to fail loudly, fail quietly, or wipe everything.
user = {'name': 'Anna', 'age': 25}
# Add or overwrite a single key
user['city'] = 'Berlin'
user['age'] = 26
# Merge many keys at once
user.update({'age': 27, 'role': 'analyst'})
# Python 3.9+ — | operator returns a new dict
merged = user | {'company': 'Stripe', 'level': 'mid'}
# Delete
data = {'a': 1, 'b': 2, 'c': 3}
del data['a'] # KeyError if 'a' is missing
val = data.pop('b') # returns 2, raises KeyError if missing
val = data.pop('z', None) # returns None, no error
data.clear() # wipes everythingThe | operator and update() differ in one important way: | returns a new dict, leaving both operands untouched. update() mutates the left-hand dict in place. In a pandas pipeline where you want to chain expressions, the | flavor reads cleanly. In a script that builds a config object once at startup, update() is fine.
Iteration and views
Iteration order is guaranteed to match insertion order since Python 3.7. This is part of the language spec, not a CPython quirk anymore, and you can rely on it in resumes and interview answers.
scores = {'sql': 85, 'python': 92, 'stats': 78}
# Default iterator yields keys
for key in scores:
print(key)
# Pairs
for key, value in scores.items():
print(f'{key}: {value}')
# Values only
for value in scores.values():
print(value)keys(), values(), and items() return view objects that reflect the dict live. Convert to a list with list(d.keys()) when you need a snapshot — particularly before mutating during iteration, which otherwise raises RuntimeError: dictionary changed size during iteration.
Dict comprehensions
Dict comprehensions are the single most-used idiom in analyst notebooks after df.groupby. They build a dict from any iterable in one expression and replace four-line for-loops.
# Squares
{x: x ** 2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Filtering
prices = {'apple': 0.80, 'mango': 3.50, 'banana': 0.60, 'avocado': 2.90}
cheap = {k: v for k, v in prices.items() if v < 2.00}
# {'apple': 0.80, 'banana': 0.60}
# Invert a dict (keys must be unique after swap)
codes = {'NYC': 'New York', 'SFO': 'San Francisco'}
inverted = {v: k for k, v in codes.items()}
# {'New York': 'NYC', 'San Francisco': 'SFO'}
# Build from two parallel lists
keys = ['revenue', 'orders', 'aov']
vals = [125_000, 1_400, 89.28]
metrics = {k: v for k, v in zip(keys, vals)}The filter clause makes dict comprehensions strictly more powerful than a dict(zip(...)) one-liner. When the source is a dataframe, dict(zip(df['code'], df['label'])) is the canonical pattern for building a lookup map — usually faster than df.set_index('code')['label'].to_dict() for small frames.
defaultdict and Counter
The collections module ships two dict subclasses that erase whole categories of bugs.
from collections import defaultdict, Counter
# Counting occurrences
word_count = defaultdict(int)
for word in ['sql', 'python', 'sql', 'sql', 'python']:
word_count[word] += 1
# defaultdict(int, {'sql': 3, 'python': 2})
# Grouping rows by key
by_city = defaultdict(list)
rows = [('Anna', 'Berlin'), ('Boris', 'Berlin'), ('Vika', 'Lisbon')]
for name, city in rows:
by_city[city].append(name)
# {'Berlin': ['Anna', 'Boris'], 'Lisbon': ['Vika']}
# Counter — purpose-built for tallies
events = ['page_view', 'click', 'page_view', 'purchase', 'click', 'click']
Counter(events)
# Counter({'click': 3, 'page_view': 2, 'purchase': 1})
Counter(events).most_common(2)
# [('click', 3), ('page_view', 2)]defaultdict(int) replaces the if key not in d: d[key] = 0 dance. defaultdict(list) is the right tool for grouping. Counter gives you .most_common(), arithmetic operators (c1 + c2, c1 - c2), and a constructor that takes any iterable.
Load-bearing trick: when you find yourself writing if key in d: d[key].append(...) else: d[key] = [...], stop and reach for defaultdict(list). Every analyst codebase has dozens of these — every one is a bug waiting for an edge case.
Analyst use cases
Three patterns cover most of what you actually do with dicts in analytics code.
Code-to-label mapping. Pull a numeric or short-string code out of SQL, look up the human label in a dict. Use .get(code, 'unknown') so a new value from upstream doesn't crash your notebook.
status_map = {0: 'new', 1: 'active', 2: 'churned'}
df['status_label'] = df['status_code'].map(status_map).fillna('unknown')Counting and grouping. You already saw Counter and defaultdict(list). In pandas you usually use groupby, but for small one-off scripts the dict version is faster to write and easier to reason about.
Query parameters and configs. Almost every analyst function ends up taking a params dict — date ranges, segment filters, metric lists. Building it as a dict literal keeps the call site readable.
query_params = {
'date_from': '2026-01-01',
'date_to': '2026-03-31',
'metrics': ['revenue', 'orders'],
'segment': 'organic',
}If you want hundreds more drills like these — dict, list comprehension, pandas groupby, SQL windows — NAILDD bundles them into one daily-practice app.
Common pitfalls
The first pitfall is the mutable default argument trap. When you write def f(tags={}):, Python evaluates the empty dict once at function definition time and reuses the same object across every call. Mutating it inside the function leaks state between invocations and produces "haunted" bugs that disappear when you restart the kernel. The fix is def f(tags=None): followed by if tags is None: tags = {} on the first line of the body. This is the most-tested Python gotcha in analytics interviews and you should be able to spot it in a code sample within five seconds.
The second pitfall is mutating a dict while iterating it. Loops like for k in d: if cond(k): del d[k] raise RuntimeError: dictionary changed size during iteration the moment they hit a real-world dataset. The fix is to iterate over a snapshot — for k in list(d.keys()): — or to build a new dict via comprehension and reassign. The latter is usually faster and more idiomatic: d = {k: v for k, v in d.items() if not cond(k)}.
The third pitfall is KeyError on optional payload fields. You're parsing a webhook from a payment processor and event['data']['customer']['email'] works on 99% of events, then crashes on the one event where the customer is anonymous. The fix is layered .get() calls with sane defaults, or — better — a single try/except KeyError around the whole extraction with a clean fallback path. Do not silence the error with a blanket except Exception; you'll mask real bugs.
The fourth pitfall is using non-string keys when serializing to JSON. json.dumps({1: 'a'}) will quietly coerce the integer key to the string '1', and json.loads will give you back a string-keyed dict. If your code does d[event_id] with integer IDs and you round-trip through JSON, every lookup will silently miss. Either convert keys to strings before serializing, or keep the dict in-process and never serialize.
The fifth pitfall is assuming keys() returns a list. In Python 3 it returns a view, and code like d.keys() + ['extra'] raises a TypeError. Wrap with list() when you need list semantics — list(d.keys()) + ['extra'] — or use unpacking: [*d.keys(), 'extra'].
Related reading
- Python cheatsheet for analysts
- Python for data analysts essential guide
- Python interview questions for data analysts
- JSON in Python cheatsheet
- Pandas cheat sheet for analysts
FAQ
When should I use a dict vs a list?
Use a dict when you need fast lookup by an identifier — user IDs to user records, country codes to country names, event types to handler functions. Use a list when order is meaningful and you access elements by position, or when you're going to iterate the whole thing anyway. If your data is naturally key-value pairs, a dict makes the intent obvious and gives you O(1) membership tests for free.
How is a dict different from JSON?
JSON is a text serialization format; dict is an in-memory data structure. json.loads(s) parses a JSON string into a Python dict, json.dumps(d) does the reverse. JSON keys must be strings; dict keys can be any hashable object. Numbers, dates, and custom classes all get coerced or rejected during serialization, so plan for that lossiness when you persist dicts through a queue or a file.
Can I sort a dict?
Dicts don't have a .sort() method, but you can build a new dict in the order you want: dict(sorted(d.items(), key=lambda kv: kv[1])) sorts by value, ascending. Since Python 3.7, insertion order is preserved, so the resulting dict iterates in your chosen order. For analyst work, this is mostly useful when you're about to dump the dict to a report and want stable, human-readable ordering.
What's the lookup cost of a Python dict?
Average O(1) thanks to hashing, worst-case O(n) under pathological collisions. In practice CPython's hash randomization and growth strategy keep real-world performance flat — you can treat dict lookups as constant-time when writing analyst code on dataframes with millions of rows.
What objects can be dict keys?
Hashable, immutable objects: str, int, float, bool, tuple of hashables, frozenset, plus user-defined classes that don't override __eq__ without __hash__. Lists, dicts, and sets cannot be keys. A common trick is to use a tuple as a composite key — metrics[('US', '2026-Q1')] = 1_250_000 — which works because tuples of strings are hashable.
Should I use OrderedDict anymore?
Rarely. Since Python 3.7, regular dicts preserve insertion order, so OrderedDict is mostly redundant. It still has one unique method — move_to_end() — and slightly different equality semantics (order-aware comparison). If you don't need either, plain dict is cleaner and faster.