To check whether a Python dictionary contains a key, use in: "state" in customer returns True or False. It checks keys, not values, and it’s the fastest way to ask:
customer = {"name": "Ava", "state": "TX"}
if "state" in customer:
print(customer["state"])
Values, get(), several keys at once and nested JSON each have a catch, so I’ll walk through them with real output from Python 3.14.7.

Check if a key exists in a Python dictionary with in
Here’s in on a small customer record, plus the method people still find in old tutorials:
customer = {"name": "Ava", "state": "TX", "zip": None, "orders": 0}
print("'state' in customer ->", "state" in customer)
print("'email' in customer ->", "email" in customer)
print("'TX' in customer ->", "TX" in customer, " (in checks KEYS, not values)")
print()
try:
customer.has_key("name")
except AttributeError as err:
print("has_key() ->", err)
Output:
'state' in customer -> True
'email' in customer -> False
'TX' in customer -> False (in checks KEYS, not values)
has_key() -> 'dict' object has no attribute 'has_key'

in checks keys only, and has_key() no longer exists."state" in customer is True because state is a key. "TX" in customer is False even though TX is in there, because it’s a value.
has_key() was removed in Python 3 and now raises AttributeError. Some tutorials still recommend it, so if you copy one and get that error, switch to in.
You’ll also see key in customer.keys(). It gives the same answer, but the .keys() part is redundant, so plain in is the idiomatic form.
Check a key without caring about upper or lower case
Keys are case-sensitive, which bites with things like HTTP headers. Compare against a lowercased set of keys:
headers = {"Content-Type": "application/json", "X-Request-Id": "8841"}
print("'content-type' in headers ->", "content-type" in headers)
lower_keys = {k.lower() for k in headers}
print("case-insensitive check ->", "content-type" in lower_keys)
Output:
'content-type' in headers -> False
case-insensitive check -> True
"content-type" isn’t a key because the real one is capitalized. Building lower_keys once and checking against it makes the test case-insensitive, and you can reuse the set for many checks.
Check if a dictionary contains a value
For values, ask the .values() view instead, or .items() for a key and value together:
customer = {"name": "Ava", "state": "TX", "zip": None, "orders": 0}
print("'TX' in customer.values() ->", "TX" in customer.values())
print("('state', 'TX') in customer.items() ->", ("state", "TX") in customer.items())
print("('state', 'CA') in customer.items() ->", ("state", "CA") in customer.items())
print()
matches = [key for key, value in customer.items() if value == "TX"]
print("keys whose value is 'TX':", matches)
Output:
'TX' in customer.values() -> True
('state', 'TX') in customer.items() -> True
('state', 'CA') in customer.items() -> False
keys whose value is 'TX': ['state']
"TX" in customer.values() finds the value, and a tuple in .items() checks that a specific key holds a specific value. That’s handy when the same value might sit under different keys.
Check for a partial match in keys or values
in needs the exact key. For a partial match, like any key containing a word, loop with any() or a comprehension:
inventory = {"iphone_16": 12, "iphone_16_pro": 4, "pixel_9": 7}
print("keys containing 'iphone':", [k for k in inventory if "iphone" in k])
print("any key starting with 'pixel'?", any(k.startswith("pixel") for k in inventory))
print("any value above 10?", any(v > 10 for v in inventory.values()))
Output:
keys containing 'iphone': ['iphone_16', 'iphone_16_pro']
any key starting with 'pixel'? True
any value above 10? True
These scan every key or value, so they’re slower than an exact in check on a big dictionary. For a few hundred entries you won’t notice.
To find which key holds a value, loop over .items() as in the last lines. Finding a key by its value covers that in depth.
Dictionary get() vs in: the None trap
get() looks like a shortcut for checking a key, and most of the time it works. Here’s where it doesn’t:
customer = {"name": "Ava", "state": "TX", "zip": None, "orders": 0}
print("customer.get('zip') ->", customer.get("zip"), " (key exists, value is None)")
print("customer.get('phone') ->", customer.get("phone"), " (key does not exist)")
print("'zip' in customer ->", "zip" in customer)
print()
if customer.get("orders"):
print("has orders")
else:
print("get('orders') looked falsy, but the key exists:", "orders" in customer)
print()
MISSING = object()
for key in ("zip", "phone"):
print(f"{key!r:<8} missing? {customer.get(key, MISSING) is MISSING}")
Output:
customer.get('zip') -> None (key exists, value is None)
customer.get('phone') -> None (key does not exist)
'zip' in customer -> True
get('orders') looked falsy, but the key exists: True
'zip' missing? False
'phone' missing? True

get() returns None both for a key set to None and for a missing key.customer.get("zip") and customer.get("phone") both return None, but only one of those keys exists. get() can’t tell you which, and in can.
The orders check is the same trap in another form. The key exists with a value of 0, and if customer.get("orders"): treats that as missing.
When you need get() and must tell the cases apart, pass a unique default like MISSING = object(). Nothing in your data can be that object, so the check is exact.
Check if a key does not exist
not in is the natural way to guard against a missing key, for example before adding a default:
customer = {"name": "Ava", "state": "TX", "zip": None, "orders": 0}
if "email" not in customer:
customer["email"] = "ava@example.com"
print(customer["email"])
print("setdefault on an existing key keeps it:", customer.setdefault("state", "CA"))
Output:
ava@example.com
setdefault on an existing key keeps it: TX
The email key didn’t exist, so it was added. setdefault() does the check and the insert in one call, and leaves an existing value alone, which is why state stayed TX.
For more ways to add entries, see adding items to a Python dictionary.
Check if a dictionary contains several keys at once
Form validation and API payloads often need a whole set of keys. .keys() behaves like a set, which makes this neat:
customer = {"name": "Ava", "state": "TX", "zip": None, "orders": 0}
required = {"name", "state", "email"}
print("has name and state? ", customer.keys() >= {"name", "state"})
print("has all required? ", customer.keys() >= required)
print("missing keys: ", required - customer.keys())
print()
print("all():", all(k in customer for k in ("name", "state")))
print("any():", any(k in customer for k in ("email", "phone", "zip")))
Output:
has name and state? True
has all required? False
missing keys: {'email'}
all(): True
any(): True

keys() >= {...} checks them all at once, and set subtraction lists what’s missing.customer.keys() >= required is true only if every required key is present. Subtracting customer.keys() from the required set gives the missing ones, ready for an error message.
Check a key across a list of dictionaries
Records from an API or a CSV usually arrive as a list of dictionaries, and not every record has every key:
orders = [
{"id": 1042, "state": "TX", "coupon": "FALL26"},
{"id": 1043, "state": "OH"},
{"id": 1044, "state": "UT", "coupon": "FALL26"},
]
print("any order has a coupon? ", any("coupon" in o for o in orders))
print("every order has a state? ", all("state" in o for o in orders))
print("orders with a coupon: ", [o["id"] for o in orders if "coupon" in o])
Output:
any order has a coupon? True
every order has a state? True
orders with a coupon: [1042, 1044]
any() answers “does at least one record have it”, all() answers “do they all”, and a comprehension lists which ones. Each check inside is a fast key lookup.
all() and any() read well for a short fixed list. I use the set version when I want to report which keys are missing, not just that something is.
Check nested keys and JSON data
JSON loads into nested dictionaries, and a missing level in the middle breaks a direct lookup:
import json
payload = json.loads('{"customer": {"name": "Ava", "address": {"city": "Austin", "zip": "78701"}}}')