Coding tips

KeyError: 'x' in Python: 10 Causes and Fixes (dict, .get, JSON, pandas tips)

Python raises KeyError when you look up a missing key with square brackets. Here are 10 real causes, from a typo and nested JSON to pandas column labels, with the exact tracebacks from my tests and the fixes that stop the crash.

By 11 min read 7 views
Cover image for KeyError: 'x' in Python: 10 Causes and Fixes (dict, .get, JSON, pandas tips)

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

#CauseHow to spot itFix
1Missing dict key with []KeyError: 'email'.get, in, or default
2Typo or wrong caseKey looks right but isn'tPrint d.keys(); normalize case
3Nested dict, wrong levelCarets under the middle lookupStep down one level at a time
4JSON field never presentCrash after json.loadsValidate keys; use .get
5Whitespace or hidden characters in keysrepr(k) shows spacesStrip / normalize keys
6Partial zip / unpack into a dictFewer values than headersCheck lengths before zipping
7Deleting keys, then reading themKey gone after pop/delpop(key, default)
8Mutating a dict while iteratingRuntimeError (related)Iterate over list(d)
9pandas Series / DataFrame label missKeyError from pandasCheck columns / use .get
10Treating a list like a dictTypeError, then a later KeyErrorUse 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) or repr(list(d)) once.
  • For JSON, list required keys and reject early.
  • For pandas, confirm columns / index labels; don't assume case.
  • Use .get / in for 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.

Share X LinkedIn WhatsApp Facebook
Was this helpful?

0%