
If you are working through a Python exercise after class, importing JSON into a small portfolio project, or trying to make an API response fit the code you wrote, you have probably seen something like this:
profile = {"name": "Ari", "plan": "free"}
print(profile["email"])
# KeyError: 'email'The quick search is usually “how to fix Python KeyError.” The useful question is more precise: is email required, optional, defaulted, or supposed to be created? The answer determines the fix.
What a KeyError actually means
The official Python exception reference defines KeyError as an error raised when a mapping key is not found among the existing keys. A dictionary is a mapping of keys to values, not a list with unusual indexes.
With bracket access, your program makes a strong claim:
email = profile["email"]That line means: “the email key must be present here.” When it is not, Python stops close to the assumption that failed. That can be helpful. An early, specific failure is often easier to diagnose than a blank value that travels through five more functions.
The Python dictionary documentation distinguishes strict subscription from get(), which returns None or a supplied default when a key is missing. These are not interchangeable styles; they express different contracts.
The contract hidden inside one pair of brackets
Imagine that profile came from a signup form. If every valid profile must contain an email address, do not turn a broken record into a “working” record:
required = {"name", "plan", "email"}
missing = required - profile.keys()
if missing:
raise ValueError(f"Missing required fields: {sorted(missing)}")
email = profile["email"]This makes the contract visible. The error says what the record lacks, rather than failing later when another feature tries to send a message.
For a beginner building a portfolio project, this distinction is more valuable than memorizing a replacement for brackets. If you can explain which fields are required and where they are validated, someone reviewing the project can understand the data flow.
When missing data is part of the input
Some fields are genuinely optional. A user may have a display name, a phone number, or neither. In that case, .get() communicates that absence is expected:
nickname = profile.get("nickname")
if nickname is None:
nickname = "Anonymous"
print(nickname)You can provide the default at the lookup:
nickname = profile.get("nickname", "Anonymous")Be careful with the meaning of the default. An empty string, None, “Anonymous,” and “unknown” are different values. If another part of the program needs to distinguish “the user did not provide a nickname” from “the nickname was deliberately blank,” use a representation that preserves that difference.
The difference between absent, empty, and null
This code does not answer the same question in both branches:
if profile.get("email"):
print("An email-like value is present")
else:
print("The value is missing or empty")It treats an empty string and a missing key as equivalent. If presence itself matters, test membership:
if "email" in profile:
print("The key exists")
else:
print("The key is absent")Then decide whether the stored value is acceptable. A key can exist with None, an empty string, or the wrong type. Presence is not validation.
When a lookup is supposed to create a value
There is a different problem when you are counting or grouping records. Suppose a study group wants to collect names by color:
from collections import defaultdict
groups = defaultdict(list)
for name, color in [
("Ari", "red"),
("Bo", "blue"),
("Cy", "red"),
]:
groups[color].append(name)
print(dict(groups))Here, a missing key should create an empty list. The official defaultdict documentation explains that its factory is called for a missing key during __getitem__, and the new value is inserted into the mapping.
That behavior is convenient for accumulation and surprising for inspection:
print(groups["green"])
print(dict(groups))
# The read created the green key.If reading should not mutate the mapping, use groups.get("green") instead. The operation should match the meaning of the data, not simply the shortest syntax.
What changes when the data comes from JSON or an API?
A dictionary created in the same file may have a clear shape. A dictionary decoded from JSON, a CSV row, or an API response has crossed a boundary. The spelling of a key may differ, a field may be omitted, or a value may have an unexpected type.
Instead of scattering defensive .get() calls throughout the application, validate the external shape once:
def parse_profile(raw: dict) -> dict:
required = {"name", "plan"}
missing = required - raw.keys()
if missing:
raise ValueError(f"Missing fields: {sorted(missing)}")
name = raw["name"]
plan = raw["plan"]
if not isinstance(name, str) or not name.strip():
raise ValueError("name must be a non-empty string")
if plan not in {"free", "pro"}:
raise ValueError("plan must be 'free' or 'pro'")
return {"name": name.strip(), "plan": plan}The rest of the program now receives a smaller, validated shape. This is useful in a beginner project because it gives you a place to explain where unreliable input becomes application data.
It also reflects a real engineering decision: fail at the boundary or let every later function guess what the input means. A short parser can prevent a long chain of vague fallbacks.
Lookup is not insertion, update, or deletion
Several dictionary operations look similar but express different intentions:
profile["email"] = "ari@example.com" # insert or replace
profile.update(plan="pro") # update a named value
plan = profile.pop("plan", "free") # remove, with an optional fallback
# del profile["email"] # delete and require the keyUse assignment when creating or replacing is acceptable. Use pop() when removing is part of the operation. Use del when the key’s presence is part of the contract and its absence should be visible.
For a small study project, write the intention beside the operation until it becomes clear. That is not unnecessary commentary; it is a way to prevent a later reader from confusing “missing is okay” with “missing indicates corrupted data.”
A note about nested dictionaries
Nested data makes the same contract question appear more than once:
order = {
"customer": {"name": "Ari"},
"payment": {"status": "paid"},
}
status = order["payment"]["status"]If payment is optional, strict access at both levels may be too strong. If a paid order must always contain payment status, early validation may be safer than chaining .get() until the result becomes impossible to interpret.
As nested dictionaries become stable domain objects, a class or dataclass may communicate the structure more clearly. See Python Classes and Object-Oriented Programming when the data needs methods and protected state. Keep a dictionary when flexible key-value data is the clearer contract.
Why “just use .get()” is incomplete advice
Replacing every bracket lookup with .get() removes one visible exception, but it does not decide what a missing key means. It may also move the failure:
email = profile.get("email")
# The problem appears later and with less context:
send_confirmation(email.lower())Now the program may raise AttributeError because email is None. The original KeyError was more informative: it identified the missing field at the lookup.
Use a default when the default is part of the domain. Use strict access when absence violates a contract. Use validation when external data must be normalized. Use defaultdict when creation-on-read is intentional. Those are four different decisions.
Write the contract before you write the fallback
Try describing one dictionary in a sentence:
“A valid profile must contain a non-empty name and either a
freeorproplan; nickname may be absent.”
That sentence tells you more than a general rule about KeyError. It tells you where to validate, which fields require brackets, which field can use .get(), and what values are valid after the lookup.
When you are debugging a small project, the useful question is not whether .get( ) looks more forgiving. Ask what the next function is allowed to assume. If the next step needs a real email address, returning None has not solved the problem; it has only moved the uncertainty.
In an introductory code review, the useful explanation is that the program received a record whose shape did not match the operation. You can then show whether the correct response is validation, an explicit default, a membership check, or deliberate key creation. The explanation is more valuable than a universal rule about replacing brackets with .get().

Alex Carter is the editorial name behind Vandutz Academy, a programming blog for beginners. Alex reviews and tests the examples and explanations published on the site, with a focus on making Python, JavaScript, web development, and developer tools easier to understand.