Python raises KeyError: 'x' when you look up a key that isn't in a mapping (usually a dict) with square brackets, as in d["x"]. The quickest fixes are d.get("x") (returns None or a default), if "x" in d before you index, setdefault / defaultdict when you're building the mapping, or a narrow try/except KeyError. For JSON and pandas, check the real keys or columns first, because a missing field or a capital letter is enough to trigger the same exception.
The Python docs list KeyError as the exception raised when a mapping (dictionary) key is not found in the set of existing keys. That's the whole story. The hard part is working out why the key isn't there: a typo, the wrong nesting level, a JSON field that never arrived, or a pandas label you thought was a column.
I reproduced every example below on Python 3.13.5, and I re-ran the basic missing-key traceback on 3.11.17 and 3.14.8. All three printed the same message and caret underlining. The pandas examples used pandas 3.0.6 on 3.13.5. Paths in the tracebacks point at my test scripts under /workspace/seo-posts-nov06/test/python/.
In this guide:
- What KeyError looks like
- The 10 causes at a glance
- 1. A missing key with square brackets
- 2. A typo or case-sensitive key
- 3. Nested dicts: the crash is often one level up
- 4. JSON that doesn't include the field you expect
- 5. Hidden whitespace or odd characters in keys
- 6. Building a dict with zip when a value is missing
- 7. Reading a key you already deleted
- 8. Mutating a dict while you iterate (related RuntimeError)
- 9. pandas Series and DataFrame KeyError
- 10. Treating a list like a dict (and the KeyError that follows)
- Reliable fixes: get, setdefault, defaultdict, try/except
- Quick checklist before you ship
- Frequently asked questions
What KeyError looks like
Here's the smallest crash. The dict has name and role, but not email:
user = {"name": "Ana", "role": "admin"}
print(user["email"])
Traceback (most recent call last):
File "/workspace/seo-posts-nov06/test/python/c01_missing.py", line 3, in <module>
print(user["email"])
~~~~^^^^^^^^^
KeyError: 'email'
Since Python 3.11, the traceback underlines the lookup. The tildes sit under user and the carets under ["email"], so you know which expression failed. The exception argument is the missing key itself: 'email'. That detail matters when you catch the error later.
The same traceback printed on 3.11.17 and 3.14.8. If you've also hit TypeError: 'NoneType' object is not subscriptable, that's a different bug: the variable was None, not a dict with a missing key. My post on TypeError: NoneType is not subscriptable covers that family of crashes.
The 10 causes at a glance
| # | Cause | How to spot it | Fix |
|---|---|---|---|
| 1 | Missing dict key with []
|
KeyError: 'email' |
.get, in, or default |
| 2 | Typo or wrong case | Key looks right but isn't | Print d.keys(); normalize case |
| 3 | Nested dict, wrong level | Carets under the middle lookup | Step down one level at a time |
| 4 | JSON field never present | Crash after json.loads
|
Validate keys; use .get
|
| 5 | Whitespace or hidden characters in keys |
repr(k) shows spaces |
Strip / normalize keys |
| 6 | Partial zip / unpack into a dict |
Fewer values than headers | Check lengths before zipping |
| 7 | Deleting keys, then reading them | Key gone after pop/del
|
pop(key, default) |
| 8 | Mutating a dict while iterating |
RuntimeError (related) |
Iterate over list(d)
|
| 9 | pandas Series / DataFrame label miss |
KeyError from pandas |
Check columns / use .get
|
| 10 | Treating a list like a dict |
TypeError, then a later KeyError |
Use integer indexes or a real dict |
1. A missing key with square brackets
Square-bracket lookup is strict. If the key isn't there, Python raises. That strictness is useful when a missing value should stop the program, and annoying when the key is optional.
Fix: use .get, test with in, or catch KeyError narrowly:
user = {"name": "Ana", "role": "admin"}
print("email" in user)
print(user.get("email"))
print(user.get("email", "n/a"))
print(user.get("name", "n/a"))
False
None
n/a
Ana
dict.get(key) returns None when the key is missing. Pass a second argument if you want a real default instead. The mapping types section of the Python built-in types docs covers get, setdefault and pop.
2. A typo or case-sensitive key
Dict keys are case-sensitive and compared by exact value. "Host" is not "host":
config = {"host": "localhost", "port": 5432}
print(config["Host"])
Traceback (most recent call last):
File "/workspace/seo-posts-nov06/test/python/c02_typo.py", line 3, in <module>
print(config["Host"])
~~~~~~^^^^^^^^
KeyError: 'Host'
Fix: print the real keys with list(config) or config.keys() before you guess. If you ingest keys from users or files, normalize them once:
cfg = {"API_KEY": "secret", " host": "x"}
print("keys repr:", [repr(k) for k in cfg])
norm = {k.strip().lower(): v for k, v in cfg.items()}
print("normalized host:", norm["host"])
print("normalized api_key:", norm["api_key"])
keys repr: ["'API_KEY'", "' host'"]
normalized host: x
normalized api_key: secret
Without normalizing, both cfg["api_key"] and cfg["host"] raised KeyError in my run because of case and a leading space. Lists, tuples, sets and dicts each compare membership differently; if you're choosing a structure, see list vs tuple vs set vs dict.
3. Nested dicts: the crash is often one level up
Chained lookups fail at the first missing key. The traceback's carets tell you which level broke:
payload = {"user": {"id": 7}}
print(payload["user"]["profile"]["avatar"])
Traceback (most recent call last):
File "/workspace/seo-posts-nov06/test/python/c03_nested.py", line 3, in <module>
print(payload["user"]["profile"]["avatar"])
~~~~~~~~~~~~~~~^^^^^^^^^^^
KeyError: 'profile'
There is no profile, so Python never reaches ["avatar"]. Fix: step down carefully, or chain .get with empty-dict defaults:
payload = {"user": {"id": 7}}
avatar = payload.get("user", {}).get("profile", {}).get("avatar")
print(avatar)
None
If a missing nest should be an error, keep the strict [] lookup and handle KeyError at the boundary of your function instead of burying defaults everywhere.
4. JSON that doesn't include the field you expect
json.loads gives you a plain dict (or list). It does not invent keys. A response that looks fine in the browser can still omit a field:
import json
raw = '{"ok": true, "items": []}'
data = json.loads(raw)
print(data["result"])
Traceback (most recent call last):
File "/workspace/seo-posts-nov06/test/python/c04_json.py", line 5, in <module>
print(data["result"])
~~~~^^^^^^^^^^
KeyError: 'result'
Fix: inspect data.keys(), prefer .get for optional fields, and validate required ones up front:
import json
data = json.loads('{"ok": true, "items": []}')
required = ("ok", "items")
missing = [k for k in required if k not in data]
if missing:
raise ValueError(f"JSON missing keys: {missing}")
result = data.get("result", None)
print("ok:", data["ok"], "result:", result)
ok: True result: None
The standard library json module documents how objects decode into dicts. Schema libraries help for bigger APIs; for small scripts, an explicit required-key check is enough.
5. Hidden whitespace or odd characters in keys
This one looks identical to a typo in the editor, but repr gives it away. A leading space, a trailing newline from a CSV, or a non-breaking space from a web form will not match the clean key you typed.
cfg = {"API_KEY": "secret", " host": "x"}
print("keys repr:", [repr(k) for k in cfg])
try:
print(cfg["host"])
except KeyError as e:
print("KeyError whitespace:", repr(e))
keys repr: ["'API_KEY'", "' host'"]
KeyError whitespace: KeyError('host')
Fix: normalize once when you build the dict (strip(), maybe lower()), and keep the rest of the code on the cleaned keys. Logging repr(list(d)) for five seconds saves an hour of staring at identical-looking strings. When you're pasting JSON payloads while debugging, the helpers on mfaysal.com/free-tools are handy for a quick look.
6. Building a dict with zip when a value is missing
dict(zip(headers, values)) stops at the shorter sequence. Missing values quietly disappear, and the later lookup raises:
headers = ["name", "role", "email"]
values = ["Ana", "admin"] # missing email
record = dict(zip(headers, values))
print("partial record:", record)
print(record["email"])
partial record: {'name': 'Ana', 'role': 'admin'}
Traceback (most recent call last):
File "/workspace/seo-posts-nov06/test/python/c08_list_as_dict.py", line 13, in <module>
print(record["email"])
~~~~~~^^^^^^^^^
KeyError: 'email'
Fix: compare lengths before you zip, or fill defaults:
headers = ["name", "role", "email"]
values = ["Ana", "admin"]
if len(values) < len(headers):
values = list(values) + [None] * (len(headers) - len(values))
record = dict(zip(headers, values))
print(record)
{'name': 'Ana', 'role': 'admin', 'email': None}
The KeyError moved from "mystery crash" to an explicit None you can test. That is usually what you want when parsing rows from a file or a spreadsheet export.
7. Reading a key you already deleted
After del or pop, the key is gone. A later [] lookup raises the same KeyError as if it never existed:
d2 = {"a": 1, "b": 2}
keys = list(d2.keys())
for k in keys:
d2.pop(k)
print("after safe pop:", d2)
print(d2["a"])
after safe pop: {}
Traceback (most recent call last):
File "/workspace/seo-posts-nov06/test/python/c16_pop_keyerror.py", line 6, in <module>
print(d2["a"])
~~^^^^^
KeyError: 'a'
Fix: use pop(key, default) when absence is normal:
user = {"name": "Ana"}
print("role:", user.pop("role", "guest"))
print("after pop:", user)
role: guest
after pop: {'name': 'Ana'}
That pattern removes the key if present and returns the default if not, without raising.
8. Mutating a dict while you iterate (related RuntimeError)
Deleting keys during a for k in d loop is not a KeyError, but it shows up in the same debugging sessions. Python refuses to keep iterating a dict whose size changed:
d = {"a": 1, "b": 2, "c": 3}
try:
for k in d:
if k == "b":
del d[k]
except RuntimeError as e:
print("RuntimeError:", e)
RuntimeError: dictionary changed size during iteration
Fix: iterate over a snapshot: for k in list(d):. Collect keys to delete, then delete them after the loop. Mixing "iterate" and "mutate" is how you get confusing crashes next to real KeyErrors.
9. pandas Series and DataFrame KeyError
pandas reuses the name KeyError when a label isn't in an index or columns. I used pandas 3.0.6:
import pandas as pd
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
print("s['b'] =", s["b"])
print(s["d"])
s['b'] = 20
KeyError: 'd'
Columns are the same story. Name is not name:
df = pd.DataFrame({"name": ["Ana", "Bo"], "score": [9, 7]})
print("columns:", list(df.columns))
print(df["Name"])
columns: ['name', 'score']
KeyError: 'Name'
Label selection with .loc and .at also raises KeyError for a missing label. Position-based .iloc does not look up by name:
print("iloc[0]:", df.iloc[0].tolist())
# df.loc[5] -> KeyError: 5
# df.at[0, "email"] -> KeyError: 'email'
iloc[0]: ['Ana', np.int64(9)]
Fix: print df.columns / s.index, use df.get("Name") for optional columns (returns None), or select by position with iloc when you mean "first row", not "row labelled 0". The pandas indexing guide covers label vs position in detail.
10. Treating a list like a dict (and the KeyError that follows)
Sometimes the first crash isn't a KeyError at all. Indexing a list with a string raises TypeError. People then convert with zip, hit cause 6, and land on KeyError anyway:
row = ["Ana", "admin"]
try:
print(row["name"])
except TypeError as e:
print("TypeError:", e)
TypeError: list indices must be integers or slices, not str
Fix: decide on one shape. If you need named fields, build a real dict (and check lengths). If you need a sequence, use integer indexes. Mixing the two is how optional CSV columns turn into production KeyErrors.
Reliable fixes: get, setdefault, defaultdict, try/except
Four patterns cover almost every dict KeyError I see:
from collections import defaultdict
user = {"name": "Ana"}
# 1) Optional read
email = user.get("email", "unknown@example.com")
# 2) Narrow catch when absence is exceptional at this layer
try:
role = user["role"]
except KeyError:
role = "guest"
# 3) setdefault while building counts
counts = {}
for word in ["a", "b", "a"]:
counts.setdefault(word, 0)
counts[word] += 1
# 4) defaultdict when missing keys should create a value
groups = defaultdict(list)
for name, team in [("Ana", "blue"), ("Bo", "red"), ("Cy", "blue")]:
groups[team].append(name)
print(email, role, counts, dict(groups))
unknown@example.com guest {'a': 2, 'b': 1} {'blue': ['Ana', 'Cy'], 'red': ['Bo']}
defaultdict is documented in the collections module. Prefer it when "missing means create", and prefer plain .get when "missing means default without inserting". Blind except Exception is worse than a narrow except KeyError, because it hides real bugs next to missing keys.
Quick checklist before you ship
- Read the caret line: which lookup failed?
- Print
list(d)orrepr(list(d))once. - For JSON, list required keys and reject early.
- For pandas, confirm
columns/ index labels; don't assume case. - Use
.get/infor optional keys; keep[]for required ones. - Don't mutate a dict while iterating it.
If the traceback instead says a module is missing, that's ModuleNotFoundError, not KeyError. See ModuleNotFoundError: No module named ... for that path.
Frequently asked questions
What does KeyError: 'x' mean in Python?
It means you used square brackets to look up the key 'x' on a mapping (usually a dict), and that key is not present. The string inside the exception is the missing key.
What is the difference between d['x'] and d.get('x')?
d['x'] raises KeyError if 'x' is missing. d.get('x') returns None instead. d.get('x', default) returns your default. Use [] for required keys and get for optional ones.
Why do I get KeyError on a nested dict when the outer key exists?
Chained lookups fail at the first missing key. payload['user']['profile'] raises KeyError: 'profile' when user exists but profile does not. Check the caret underline in the traceback.
How do I fix KeyError after json.loads?
json.loads only creates keys that were in the JSON text. Print data.keys(), require the fields you need, and use .get for optional ones.
Why does pandas raise KeyError for a column I can see?
Column names are exact. 'Name' is not 'name'. Print list(df.columns). For optional columns, df.get('Name') returns None instead of raising.
Is catching KeyError a bad practice?
A narrow except KeyError at a boundary is fine. A bare except or except Exception that swallows everything is not. Prefer .get or 'in' when absence is a normal case.
KeyError vs TypeError: list indices must be integers?
If you index a list with a string, you get TypeError, not KeyError. KeyError is for missing keys on mappings (and pandas labels). Convert to a dict only when you have matching headers and values.
Does KeyError happen with sets or tuples?
Sets and tuples are not mappings. Looking up with [] on a set fails differently; on a tuple you use integer indexes. KeyError is the missing-key exception for dict-like objects.
Last checked: 6 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.
Originally published on mfaysal.com.
I'm learning cybersecurity on TryHackMe — follow my progress at tryhackme.com/p/mfaysal.dev.
Top comments (0)