Python Dictionary Methods That Clean Up Your Code
get(), setdefault(), update(), comprehensions, defaultdict, Counter, and practical patterns for writing cleaner Python with dictionaries.
Python dictionaries are the workhorse data structure. You'll use them for config objects, caching, counting, grouping, and as lightweight records. Most people know the basics -- d[key] = value, key in d -- but there's a layer of methods and patterns that separates clunky dictionary code from clean dictionary code.
get(): Stop Writing If-Else for Missing Keys
The most common dictionary pattern I see in beginner code:
if "name" in user:
name = user["name"]
else:
name = "Unknown"
Just use get():
name = user.get("name", "Unknown")
If the key exists, you get its value. If not, you get the default (which is None if you don't specify one). One line, no exception risk.
Where this really shines -- nested lookups:
# Instead of checking each level
city = config.get("database", {}).get("host", {}).get("city", "N/A")
Though honestly, if you're going more than two levels deep, consider restructuring your data or using a library like glom.
setdefault(): Get-or-Initialize in One Call
Say you're grouping items by category:
groups = {}
for item in items:
category = item["category"]
if category not in groups:
groups[category] = []
groups[category].append(item)
setdefault() collapses the if-check:
groups = {}
for item in items:
groups.setdefault(item["category"], []).append(item)
setdefault(key, default) returns the existing value if the key is present. If the key is missing, it inserts the default and returns it. So you can chain methods onto the return value directly.
It's one of those methods that reads oddly at first but becomes second nature fast.
defaultdict: When setdefault Gets Tedious
If you're using setdefault with the same default type everywhere, defaultdict from the collections module is cleaner:
from collections import defaultdict
groups = defaultdict(list)
for item in items:
groups[item["category"]].append(item)
Accessing a missing key automatically creates it with the factory function you passed (list creates [], int creates 0, set creates set()).
Counting pattern:
counts = defaultdict(int)
for word in words:
counts[word] += 1
No KeyError, no initialization checks. But there's something even better for counting.
Counter: Purpose-Built for Counting
from collections import Counter
words = ["python", "java", "python", "rust", "python", "java"]
counts = Counter(words)
print(counts)
# Counter({'python': 3, 'java': 2, 'rust': 1})
print(counts.most_common(2))
# [('python', 3), ('java', 2)]
Counter is a dict subclass that's built for this exact use case. Beyond basic counting, it supports arithmetic:
survey_a = Counter({"python": 45, "java": 30, "rust": 15})
survey_b = Counter({"python": 50, "java": 25, "go": 20})
combined = survey_a + survey_b
# Counter({'python': 95, 'java': 55, 'go': 20, 'rust': 15})
difference = survey_a - survey_b
# Counter({'java': 5, 'rust': 15}) -- drops zero/negative
most_common() alone makes Counter worth using. Sorting a dictionary by value manually is annoying; Counter just gives it to you.
update(): Merging Dictionaries
defaults = {"theme": "dark", "language": "en", "page_size": 25}
user_prefs = {"theme": "light", "page_size": 50}
config = defaults.copy()
config.update(user_prefs)
# {'theme': 'light', 'language': 'en', 'page_size': 50}
update() overwrites existing keys and adds new ones. It modifies the dictionary in place (returns None), so always .copy() first if you need to preserve the original.
Since Python 3.9, you can also use the merge operator:
config = defaults | user_prefs # new dict
defaults |= user_prefs # in-place update
The | operator reads more naturally and doesn't require the copy step.
items(), keys(), values(): Iteration Patterns
These return view objects, not lists. They reflect changes to the dictionary and are memory-efficient.
settings = {"debug": True, "verbose": False, "timeout": 30}
# Iterate over key-value pairs
for key, value in settings.items():
print(f"{key} = {value}")
# Check if a value exists (less common but useful)
if 30 in settings.values():
print("Something has a 30")
# Get all keys as a set for comparison
shared_keys = settings.keys() & other_settings.keys()
That last one is worth knowing -- dict.keys() supports set operations. You can find shared keys, differing keys, etc. without converting to sets manually.
Dictionary Comprehensions
Like list comprehensions, but for dicts. They replace a surprising amount of loop-and-accumulate code:
# Invert a dictionary
names = {"alice": 1, "bob": 2, "carol": 3}
ids = {v: k for k, v in names.items()}
# {1: 'alice', 2: 'bob', 3: 'carol'}
# Filter a dictionary
config = {"debug": True, "verbose": False, "timeout": 30, "retries": 0}
truthy = {k: v for k, v in config.items() if v}
# {'debug': True, 'timeout': 30}
# Transform values
prices = {"apple": 1.50, "banana": 0.75, "mango": 2.00}
discounted = {k: round(v * 0.9, 2) for k, v in prices.items()}
# {'apple': 1.35, 'banana': 0.68, 'mango': 1.8}
Keep them readable. If the comprehension needs an if, an else, and a nested loop, just write a regular loop. Clever one-liners that take 30 seconds to parse aren't clever.
Practical Patterns
Building a lookup table from a list of records:users = [
{"id": 1, "name": "Priya"},
{"id": 2, "name": "James"},
{"id": 3, "name": "Sara"},
]
user_by_id = {u["id"]: u for u in users}
# O(1) lookup instead of scanning the list every time
print(user_by_id[2]["name"]) # "James"
This is one of the highest-impact patterns in Python. If you're doing next(u for u in users if u["id"] == target) repeatedly, convert to a dict first.
from collections import Counter
logs = [
{"status": 200}, {"status": 404}, {"status": 200},
{"status": 500}, {"status": 200}, {"status": 404},
]
status_counts = Counter(log["status"] for log in logs)
# Counter({200: 3, 404: 2, 500: 1})
Safe nested access with a helper:
def deep_get(d, *keys, default=None):
for key in keys:
if isinstance(d, dict):
d = d.get(key)
else:
return default
return d if d is not None else default
config = {"db": {"primary": {"host": "localhost"}}}
host = deep_get(config, "db", "primary", "host") # "localhost"
port = deep_get(config, "db", "primary", "port", default=5432) # 5432
Getting Comfortable
Dictionaries show up everywhere in Python -- API responses, config files, database rows, function kwargs. The difference between writing five lines of key-checking boilerplate and a clean one-liner with get() or a comprehension adds up across an entire codebase.
If you want to practice these patterns with actual problems, CodeUp has Python challenges that lean heavily on dictionary manipulation -- data transformation, frequency analysis, nested data parsing. Working through them is the fastest way to make these methods second nature instead of something you have to look up every time.