AttributeError: 'dict' object has no attribute 'X'
1 · The lesson
readWhat this error means
You used dot access — user.name — on a dictionary. Python dicts don't expose their keys as attributes; you reach a value with user["name"] or user.get("name"). The other common cause is a Python 2 method name, such as iteritems or has_key, that was removed in Python 3.
When you see it
Dot access on data that came from JSON:
Traceback (most recent call last): File "main.py", line 5, in <module> print(user.name) ^^^^^^^^^ AttributeError: 'dict' object has no attribute 'name'
Old Python 2 code running on Python 3:
Traceback (most recent call last): File "main.py", line 3, in <module> for item, qty in stock.iteritems(): ^^^^^^^^^^^^^^^ AttributeError: 'dict' object has no attribute 'iteritems'
Why it happens
Dot access is for attributes, brackets are for keys. JavaScript blurs the two — user.name and user["name"] are the same thing there — so it's an easy habit to bring over. Python keeps them apart. A dict's attributes are its methods (keys, items, get, update…). Its data lives behind the brackets.
import json response = '{"name": "Grace", "role": "admin"}' user = json.loads(response) print(user.name) # AttributeError: no attribute 'name'
There's a nastier variant: when the key happens to share a name with a dict method, dot access doesn't fail at all. It hands you the method.
order = {"id": 1042, "items": ["keyboard", "mouse"]}
print(order.items) # <built-in method items of dict object at 0x...>
print(order["items"]) # ['keyboard', 'mouse']No error, wrong value. That alone is a good reason not to try to make dicts dot-accessible.
Python 2 method names. These were removed in Python 3:
| Python 2 | Python 3 |
|---|---|
d.iteritems() | d.items() |
d.iterkeys() | d.keys() or just d |
d.itervalues() | d.values() |
d.has_key(k) | k in d |
d.viewitems() | d.items() (it's already a view) |
In Python 3, items(), keys() and values() return lightweight views, not copies, so the iter* versions had no reason to stay.
How to fix it
Use brackets when the key must be there, .get() when it might not. A missing key with brackets raises KeyError, which is what you want for required fields. .get() returns None, or a default you choose:
import json user = json.loads('{"name": "Grace", "role": "admin"}') print(user["name"]) # Grace print(user.get("team", "unassigned")) # unassigned
Translate the Python 2 calls:
stock = {"keyboard": 12, "mouse": 40, "monitor": 3}
for item, qty in stock.items():
print(f"{item:<10}{qty:>4}")
if "mouse" in stock:
print("mouse in stock")If you want dot access, convert to a real object — on purpose. For a quick script, types.SimpleNamespace turns a dict into something with attributes. object_hook applies it to every nested object while parsing:
import json from types import SimpleNamespace response = '{"name": "Grace", "address": {"city": "Arlington", "zip": "22201"}}' user = json.loads(response, object_hook=lambda d: SimpleNamespace(**d)) print(user.name) # Grace print(user.address.city) # Arlington
For anything that lives longer than a script, define the shape with a dataclass. You get dot access, a readable repr, and an error at construction time if a field is missing — instead of a KeyError buried three calls deep:
import json from dataclasses import dataclass @dataclass class Invoice: number: str customer: str total: float payload = '{"number": "INV-2041", "customer": "Linus", "total": 312.40}' invoice = Invoice(**json.loads(payload)) print(invoice.customer) # Linus print(invoice) # Invoice(number='INV-2041', customer='Linus', total=312.4)
When the data comes from an API you don't control, a validation library such as pydantic does the same job and also checks the types. For your own code, a dataclass is usually enough.
I'd steer clear of the "AttrDict" recipes that subclass dict and override __getattr__. They work until a key is called items, keys, copy or update, or contains a hyphen, and then you're back to brackets anyway.
When you'd actually see this in real code
requests.get(url).json()returns plain dicts and lists. Code written against a typed SDK (response.data.id) breaks when you swap to raw HTTP.- A Flask or Django view reads
request.json.emailinstead ofrequest.json["email"]. - Code ported from Python 2 — old tutorials, inherited scripts — still calls
.iteritems()or.has_key(). - A YAML or TOML config loaded with
yaml.safe_loadortomllib: both give you dicts, soconfig.database.hostfails. - A function that used to return an object was refactored to return a dict, and one caller still uses dot access.
Related errors
- KeyError: 'X' — what you get after switching to brackets, if the key isn't actually there.
- AttributeError: 'str' object has no attribute 'X' — the step before this one: the JSON hasn't been parsed yet.
- AttributeError: 'NoneType' object has no attribute 'X' —
.get()returnedNoneand you chained a method onto it. - AttributeError: 'list' object has no attribute 'X' — the API returned a list of dicts, not one dict.
- RuntimeError: dictionary changed size during iteration
See Also
- All Python errors — the full index, by type and by when it happens.
- Dictionaries (tutorial)
- Python Dictionary Cheat Sheet
- Dataclasses
- Consuming REST APIs
Found this useful? Share it with a friend who’s learning Python.