A Python KeyError is raised when you access a dictionary with a key that doesn't exist. It's one of the most common runtime errors — especially when parsing API responses, JSON payloads, or configuration files. This guide covers every pattern you need to handle it safely.
What is a KeyError?
When you access dict[key] and that key doesn't exist, Python raises a KeyError. The error message shows the missing key name.
user = {"name": "Alice", "email": "alice@example.com"}
# This raises KeyError: 'age'
print(user["age"])
# Traceback:
# KeyError: 'age'Fix 1: Use dict.get() — The Safest Pattern
The .get() method returnsNone if the key is missing, or a default value you specify. It never raises a KeyError.
user = {"name": "Alice"}
# Returns None — no error
age = user.get("age")
# Returns a default value
age = user.get("age", 0)
role = user.get("role", "viewer")
print(age) # 0
print(role) # "viewer"Fix 2: Check with 'in' Before Accessing
Use the in operator to guard access. Best when you need different logic for present vs absent keys.
data = {"status": "active", "score": 95}
if "score" in data:
print(f"Score: {data['score']}")
else:
print("Score not available")
# Combine with walrus operator (Python 3.8+)
if (score := data.get("score")) is not None:
print(f"Score: {score}")Fix 3: try/except KeyError
Use try/except when you want to log the error or handle it differently from other exceptions. Always log the full payload for debugging.
import logging
def get_user_email(data: dict) -> str:
try:
return data["user"]["email"]
except KeyError as e:
logging.warning("Missing key in user data: %s | payload: %s", e, data)
return ""
result = get_user_email({"user": {"name": "Bob"}})
print(result) # "" — no crash, warning loggedFix 4: Nested Dictionaries — Chain get()
For nested dicts, chain .get() calls. Each intermediate .get("key", ) returns an empty dict on a missing key, so the next .get() has something to call on.
response = {
"data": {
"user": {
"profile": {"city": "New York"}
}
}
}
# Safe — returns "" if any level is missing
city = (
response
.get("data", {})
.get("user", {})
.get("profile", {})
.get("city", "")
)
print(city) # "New York"
# Missing path — no KeyError, returns ""
country = (
response
.get("data", {})
.get("user", {})
.get("profile", {})
.get("country", "")
)
print(country) # ""Fix 5: Use defaultdict for Counters
defaultdict from the collections module auto-creates a default value when a missing key is accessed — no KeyError, no pre-initialization needed.
from collections import defaultdict
# Count word frequency — no KeyError on first encounter
word_counts = defaultdict(int)
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
for word in words:
word_counts[word] += 1
print(dict(word_counts))
# {'apple': 3, 'banana': 2, 'cherry': 1}
# Group items by category
grouped = defaultdict(list)
items = [("fruit", "apple"), ("veg", "carrot"), ("fruit", "banana")]
for category, item in items:
grouped[category].append(item)
print(dict(grouped))
# {'fruit': ['apple', 'banana'], 'veg': ['carrot']}Fix 6: setdefault() for Auto-Initialization
setdefault(key, default) sets a key only if it doesn't exist yet, then returns the value. Useful for building nested structures incrementally.
# Build a nested dict without KeyError
index = {}
records = [("UK", "London"), ("UK", "Manchester"), ("US", "New York")]
for country, city in records:
index.setdefault(country, []).append(city)
print(index)
# {'UK': ['London', 'Manchester'], 'US': ['New York']}
# Also useful for caching
cache = {}
def get_config(key: str) -> dict:
return cache.setdefault(key, {"loaded": False})Fix 7: API Responses — Always Validate Structure
Never assume an API response has all the expected keys. Check the shape of the response before accessing nested keys.
import requests
def fetch_user(user_id: int) -> dict:
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
data = response.json()
# Validate structure before accessing
if not isinstance(data, dict):
raise ValueError(f"Expected dict, got {type(data)}")
user = data.get("user") or {}
return {
"id": user.get("id", user_id),
"name": user.get("name", "Unknown"),
"email": user.get("email", ""),
"role": user.get("role", "viewer"),
}KeyError in API Responses — Real Example
APIs often return different shapes depending on success, error state, or version. Here's a defensive parser for a typical paginated API response:
Fragile — crashes on unexpected shape:
# KeyError if "results" or "items" is absent data = response.json() users = data["results"]["items"] total = data["results"]["total_count"]
Defensive — handles missing keys gracefully:
import logging
def parse_paginated_response(data: dict) -> dict:
results = data.get("results") or {}
return {
"items": results.get("items") or [],
"total": results.get("total_count", 0),
"page": results.get("page", 1),
"has_more": results.get("has_more", False),
}
try:
data = response.json()
parsed = parse_paginated_response(data)
except Exception as e:
logging.error("Failed to parse response: %s | body: %s", e, response.text[:500])
parsed = {"items": [], "total": 0, "page": 1, "has_more": False}Python vs JavaScript — Similar Error, Different Fix
| Language | Error | Safe Access |
|---|---|---|
| Python | KeyError: 'key' | dict.get('key', default) |
| JavaScript | TypeError: Cannot read properties of undefined | obj?.key ?? 'default' |
Validate your JSON API responses
Paste your JSON and instantly check its structure before writing Python parsing code.
Validate your JSON API responses →Frequently Asked Questions
Why does Python raise KeyError when the key looks like it exists?
Python raises KeyError when the exact key string is not found — even a trailing space, different capitalisation, or a Unicode lookalike makes it a different key. Printrepr(key) andlist(data.keys()) to compare them character by character. In API responses, the key may also be absent for certain records — usedata.get("key") to return None safely.
What is the difference between dict[key] and dict.get(key)?
dict[key] raises KeyError if the key is absent. dict.get(key) returns None, and dict.get(key, default) returns your fallback value. Use .get() when the key's presence is uncertain.
How do I handle KeyError in a loop?
Use dict.get(key, default) inside the loop, or use defaultdict from collections when building aggregations. If you need per-item error handling, wrap access in try/except and log the failing item.
How do I stop KeyError when building a counter or grouping dict?
Use defaultdict from collections.defaultdict(int) auto-initialises missing keys to 0 — so counts[key] += 1 never raises KeyError. defaultdict(list) auto-initialises to an empty list so grouping with groups[key].append(value) is always safe.
How do I fix KeyError in a nested dictionary?
Chain .get() calls with empty dict fallbacks: data.get("a", ).get("b", ""). Each level returns a safe empty dict if the key is missing, so the chain never raises KeyError.