Python dictionaries are the most versatile built-in data structure. Every real-world program uses them constantly — JSON data, config objects, lookup tables, counting, grouping. This guide covers everything from basics to advanced patterns.
Quick Reference
| Operation | Syntax | Returns |
|---|---|---|
| Create | d = {"key": "value"} |
dict |
| Get value | d["key"] or d.get("key") |
value or KeyError |
| Set value | d["key"] = value |
— |
| Delete key | del d["key"] or d.pop("key") |
— or value |
| Check key | "key" in d |
bool |
| All keys | d.keys() |
dict_keys |
| All values | d.values() |
dict_values |
| All pairs | d.items() |
dict_items |
| Length | len(d) |
int |
| Merge (3.9+) | d1 | d2 |
new dict |
| Update | d.update(other) |
— |
Creating Dictionaries
# Literal syntax
user = {"name": "Ana", "age": 30, "active": True}
# Empty dict
empty = {}
empty = dict()
# From keyword arguments
config = dict(host="localhost", port=5432, debug=False)
# From list of (key, value) pairs
pairs = [("a", 1), ("b", 2), ("c", 3)]
d = dict(pairs)
# From two lists with zip
keys = ["x", "y", "z"]
values = [10, 20, 30]
d = dict(zip(keys, values))
# All keys with the same default value
scores = dict.fromkeys(["alice", "bob", "carol"], 0)
# {"alice": 0, "bob": 0, "carol": 0}
Accessing Values
Square bracket vs .get()
user = {"name": "Ana", "age": 30}
# Square bracket — raises KeyError if missing
name = user["name"] # "Ana"
city = user["city"] # KeyError: 'city'
# .get() — returns None (or default) if missing
city = user.get("city") # None
city = user.get("city", "unknown") # "unknown"
Rule: Use d["key"] when the key must exist (fail fast). Use d.get(key, default) when the key might be absent.
Nested access
data = {"user": {"address": {"city": "Podgorica"}}}
# Chained access — KeyError if any level missing
city = data["user"]["address"]["city"] # "Podgorica"
# Safe nested access with get()
city = data.get("user", {}).get("address", {}).get("city", "unknown")
Modifying Dictionaries
user = {"name": "Ana", "age": 30}
# Set / update a value
user["age"] = 31
user["email"] = "ana@example.com" # add new key
# Delete a key
del user["email"]
# Remove and return a value
age = user.pop("age") # returns 30, removes key
val = user.pop("missing", 0) # returns 0, no error
# Remove and return last inserted pair (3.7+ preserves insertion order)
key, val = user.popitem()
# Clear all keys
user.clear()
# Set default if key missing, leave it if present
user.setdefault("role", "viewer") # sets "role": "viewer" only if absent
Merging Dictionaries
defaults = {"debug": False, "timeout": 30, "retries": 3}
overrides = {"timeout": 60, "verbose": True}
# Python 3.9+ — | operator (non-destructive)
config = defaults | overrides
# {"debug": False, "timeout": 60, "retries": 3, "verbose": True}
# Python 3.9+ — |= update in place
defaults |= overrides
# Python 3.5+ — unpacking (works everywhere)
config = {**defaults, **overrides}
# .update() — modifies in place
defaults.update(overrides)
Right side wins on duplicate keys in all four methods.
Iterating Dictionaries
inventory = {"apples": 5, "bananas": 12, "oranges": 0}
# Keys only (default)
for key in inventory:
print(key)
# Values only
for count in inventory.values():
print(count)
# Keys and values together — use .items()
for item, count in inventory.items():
print(f"{item}: {count}")
# Sorted by key
for item in sorted(inventory):
print(item, inventory[item])
# Sorted by value (descending)
for item, count in sorted(inventory.items(), key=lambda kv: kv[1], reverse=True):
print(item, count)
Dictionary Comprehensions
# Basic: square every number
squares = {n: n**2 for n in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# With filter: only even squares
even_squares = {n: n**2 for n in range(1, 11) if n % 2 == 0}
# Invert a dictionary (flip keys and values)
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
# {1: "a", 2: "b", 3: "c"}
# Filter keys from an existing dict
big_only = {k: v for k, v in inventory.items() if v > 5}
# Transform values
upper_keys = {k.upper(): v for k, v in original.items()}
# From a list of objects
users = [{"id": 1, "name": "Ana"}, {"id": 2, "name": "Ben"}]
user_by_id = {u["id"]: u for u in users}
# {1: {"id": 1, "name": "Ana"}, 2: {...}}
Counting and Grouping
Counter pattern
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
# Manual counter
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
# {"apple": 3, "banana": 2, "cherry": 1}
# Cleaner with setdefault
counts = {}
for word in words:
counts.setdefault(word, 0)
counts[word] += 1
# Best: collections.Counter
from collections import Counter
counts = Counter(words)
counts.most_common(2) # [("apple", 3), ("banana", 2)]
Grouping pattern
records = [
{"city": "NYC", "score": 90},
{"city": "LA", "score": 85},
{"city": "NYC", "score": 75},
]
# Group records by city
from collections import defaultdict
by_city = defaultdict(list)
for r in records:
by_city[r["city"]].append(r["score"])
# {"NYC": [90, 75], "LA": [85]}
Nested Dictionaries
# Build a nested structure
company = {
"engineering": {
"backend": ["Ana", "Ben"],
"frontend": ["Carol"],
},
"marketing": {
"seo": ["Dave"],
},
}
# Access
backend_team = company["engineering"]["backend"] # ["Ana", "Ben"]
# Safe add without overwriting
company.setdefault("sales", {}).setdefault("outbound", []).append("Eve")
# Flatten to a list of (dept, subdept, member) tuples
flat = [
(dept, subdept, member)
for dept, teams in company.items()
for subdept, members in teams.items()
for member in members
]
Common Patterns
Default factory with defaultdict
from collections import defaultdict
# Avoid KeyError on first access
word_positions = defaultdict(list)
for i, word in enumerate("the quick brown fox".split()):
word_positions[word].append(i)
# {"the": [0], "quick": [1], "brown": [2], "fox": [3]}
# Integer default (starts at 0)
counts = defaultdict(int)
counts["missing"] += 1 # no KeyError, starts at 0
Ordered dict (3.7+ dicts are ordered)
Python 3.7+ guarantees insertion order in regular dicts. collections.OrderedDict is only needed when you need move_to_end() or want to signal ordering intent explicitly.
ChainMap for layered lookups
from collections import ChainMap
system_env = {"DEBUG": "false", "LOG_LEVEL": "error"}
project_env = {"LOG_LEVEL": "info"}
local_env = {"DEBUG": "true"}
config = ChainMap(local_env, project_env, system_env)
config["DEBUG"] # "true" — local wins
config["LOG_LEVEL"] # "info" — project wins over system
Performance Tips
| Operation | Time complexity |
|---|---|
d[key] |
O(1) average |
key in d |
O(1) average |
d[key] = val |
O(1) average |
del d[key] |
O(1) average |
len(d) |
O(1) |
| Iterate all keys | O(n) |
- Prefer
d.get(key, default)overif key in d: d[key]— one lookup, not two. - Use
Counterfor frequency counting instead of a manual dict. - Use
defaultdictto eliminatesetdefaultboilerplate in loops. - When you need an immutable mapping (for hashing), use
types.MappingProxyType(d).
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
d["missing"] |
KeyError crash | d.get("missing", default) |
| Mutating dict while iterating | RuntimeError | Iterate over list(d.keys()) or build a new dict |
dict.fromkeys(keys, []) |
All keys share the same list | Use comprehension: {k: [] for k in keys} |
Comparing d.keys() with a set |
Works but unclear | Use set(d) == other_set |
| Using a list as a key | TypeError (unhashable) | Use a tuple instead |
{**d1, **d2} key order |
Right side wins silently | Document merge order clearly |
The mutable default trap
# WRONG — all keys share one list object
bad = dict.fromkeys(["a", "b", "c"], [])
bad["a"].append(1)
print(bad) # {"a": [1], "b": [1], "c": [1]} ← bug!
# CORRECT — each key gets its own list
good = {k: [] for k in ["a", "b", "c"]}
good["a"].append(1)
print(good) # {"a": [1], "b": [], "c": []} ✓
Iterating while mutating
d = {"a": 1, "b": 2, "c": 3}
# WRONG
for key in d:
if d[key] < 2:
del d[key] # RuntimeError: dictionary changed size during iteration
# CORRECT — snapshot keys first
for key in list(d.keys()):
if d[key] < 2:
del d[key]
# Or build a new dict
d = {k: v for k, v in d.items() if v >= 2}
FAQ
What's the difference between d[key] and d.get(key)?d[key] raises KeyError if the key is absent. d.get(key) returns None (or a provided default). Use d[key] when absence is a programming error; use .get() when absence is expected.
Are Python dicts ordered?
Yes — since Python 3.7, dicts preserve insertion order as a language guarantee (CPython 3.6 already did it as an implementation detail). You don't need OrderedDict for ordering anymore.
Can I use a list as a dictionary key?
No. Dict keys must be hashable. Lists are mutable and unhashable. Use a tuple instead: d[(1, 2)] = "pair".
How do I copy a dict safely?d.copy() or dict(d) creates a shallow copy — nested objects are still shared. For a full deep copy: import copy; copy.deepcopy(d).
What's the fastest way to count occurrences?collections.Counter(iterable) — it's implemented in C and is faster than a manual loop.
How do I merge two dicts in Python 3.8 and earlier?{**d1, **d2} works in Python 3.5+. .update() works in all versions. The | operator requires Python 3.9+.