skip to content

What does getattr(obj, 'x', None) give you that plain obj.x does not?

level: juniorimportance: must knowfreq 70%

answer

  1. The name arrives as data
  2. Three arguments, not two
  3. The third one swallows something
  4. Write and delete halves exist too
  5. Only AttributeError is suppressed

basics

~20 s

getattr takes the attribute name as a runtime string, and its optional third argument is returned instead of raising AttributeError when the name is absent. Dotted syntax needs the name written in the source and always raises.

solid answer

~40 s

`getattr(obj, name)` performs exactly the lookup `obj.name` performs — same descriptors, same walk up the class hierarchy — except the name arrives as an ordinary `str` computed at run time, so it can come from a column header, a config key or a registry table. The three-argument form `getattr(obj, name, default)` returns `default` instead of raising `AttributeError`, which makes it the idiomatic "read this if it is there" probe. `setattr(obj, name, value)` and `delattr(obj, name)` are the write and delete halves of the same idea. Two limits matter: the default suppresses `AttributeError` only, so any other exception raised during the lookup still reaches the caller, and the name must be a `str` — anything else raises `TypeError`, not `AttributeError`.

code

python · 15 lines
python
class Message:
    def __init__(self, body):
        self.body = body

m = Message("hi")
print(getattr(m, "body"))
print(getattr(m, "edited_at", None))
setattr(m, "edited_at", "2026-09-05")
print(m.__dict__)
delattr(m, "edited_at")
print(hasattr(m, "edited_at"))
try:
    getattr(m, 42, None)
except TypeError as exc:
    print("TypeError:", exc)

go deeper

for a junior

Be ready to write the three-argument form from memory and say what its default protects against. Know that setattr and delattr are the write and delete halves of the same idea, and that the name is just a string.

for a middle

Explain that the function form runs the identical lookup as dotted syntax, only with a runtime name, and that the default suppresses AttributeError alone. Know the failure modes of setattr on slotted or read-only attributes.

for a senior

Show where a sprayed-on default turns a real failure into a quietly missing field, and how you keep name-driven reads auditable when a serializer or a registry is driving them across a large codebase.

for a principal

Own the design call: when attribute names start arriving as data from files or requests, decide whether a mapping or a declared schema is the honest structure, and where reflection over objects earns its keep versus hides a missing contract.

## The same lookup, with the name as data `obj.x` and `getattr(obj, "x")` are the same operation. Both run the full attribute lookup: data descriptors on the type first, then the instance's own `__dict__`, then plain class attributes and non-data descriptors up the method resolution order, and finally the type's fallback hook for missing names if it defines one. Nothing about the semantics changes. The one difference is *when the name is known*. The dotted form bakes the name into the compiled bytecode, so it must be written in the source. The builtin takes the name as an ordinary string argument, so it can be a value — a column header from a file, a key out of a config mapping, a field name stored in a registry, a string built two hundred lines away. That is the entire reason the builtin exists, and it is why every attribute-name-driven design in Python — serializers, object-to-row mappers, plugin registries, generic comparison helpers — bottoms out in it. ## The three-argument form `getattr(obj, name, default)` is defined as: do the lookup; if it raises `AttributeError`, return `default` instead. It is exactly this, written as an expression: ```python try: value = obj.x except AttributeError: value = default ``` Two consequences follow directly from that definition, and both are commonly missed. First, **only `AttributeError` is suppressed.** If reading the attribute runs code — a computed attribute, a lazy loader, any descriptor — and that code raises `ValueError`, `OSError` or `KeyError`, the default does not save you; the exception propagates. That is the correct design, but it surprises people who read the third argument as "never fail". Second, **the suppression is indiscriminate about *where* the `AttributeError` came from.** If a computed attribute's own body raises `AttributeError`, the default is returned and a real bug is silently reported as a missing field. This is the single largest hazard of the three-argument form, and the reason a default should be reserved for names you genuinely expect to be optional rather than sprayed over every dynamic read. ## setattr and delattr `setattr(obj, name, value)` is the assignment half: it is `obj.name = value` with the name as data. It respects the same machinery, so it can fail where plain assignment fails — on an instance of a class with `__slots__` that does not list the name, or when the type defines the attribute as a read-only computed one: ```python class S: __slots__ = ("a",) setattr(S(), "b", 1) # AttributeError: 'S' object has no attribute 'b' and no __dict__ for setting new attributes ``` `delattr(obj, name)` is `del obj.name`. It removes the name from the *instance*; it cannot reach through to the class. Deleting a name the instance never carried — one that reads fine because it is defined on the class — raises `AttributeError`, which trips people who expect a delete to be idempotent. If an instance attribute was shadowing a class attribute, deleting it makes the class value visible again rather than making the name disappear. Unlike `getattr`, neither `setattr` nor `delattr` accepts a default; there is nothing to default to. ## Name typing and validation The name argument must be a `str`. Passing an `int`, `bytes` or `None` raises `TypeError: attribute name must be string`, not `AttributeError` — so a `try/except AttributeError` around a dynamic read will not catch a name that arrived in the wrong type from a parser. Nor is the name validated as an identifier: `setattr(obj, "has space", 1)` succeeds and puts an unreachable-by-syntax key in the instance dictionary, readable only through `getattr` afterwards. ## Cost, and when to reach for something else Since 3.11 CPython's specializing interpreter optimizes the dotted attribute instruction with an inline cache keyed to the object's shape. A call to the builtin is an ordinary function call and does not get that treatment, so in a hot loop the function form is measurably slower than the dotted form. Write `obj.x` when you know the name; that is not style advice, it is the faster path. The deeper judgement call is whether attribute names should be data at all. When names are arriving from outside — a file, a request, a config — a plain `dict` is often the honest structure, and reaching for reflection over objects is a sign that a schema is missing. Reflection is the right tool when the *objects* are the source of truth (a registry of handler classes, an object-to-record mapper) and the wrong tool when it is only being used to avoid writing the mapping down.

  • Does the default in getattr(obj, name, default) suppress every exception the lookup can raise?
    No — only `AttributeError`. If the attribute is computed and its code raises `ValueError`, `KeyError` or `OSError`, that exception propagates to the caller untouched. The default is defined as a `try/except AttributeError`, nothing broader, which is also why an `AttributeError` raised *inside* a computed attribute is silently reported as a missing name.
  • What happens if the name you pass to getattr is not a str?
    It raises `TypeError: attribute name must be string`, not `AttributeError`, and the three-argument default does not catch it. So a name that arrived in the wrong type from a parser escapes a `try/except AttributeError` guard. The name is also not checked as a valid identifier — `setattr(obj, "has space", 1)` succeeds and stores a key that no dotted expression can ever read back.
  • When does delattr(obj, name) raise even though obj.name reads fine?
    When the name lives on the class rather than in the instance's own dictionary. `delattr` removes an instance attribute; it will not reach up and delete a class attribute, so it raises `AttributeError` for a name the instance is merely inheriting. If the instance had shadowed a class attribute, deleting the instance entry uncovers the class value again instead of removing the name.

Dotted access is dialling a number you memorised; getattr is dialling a number read off a card someone hands you at run time — same phone system, but the digits arrive later, and you can decide what to do when nobody answers.

saying these in an interview costs you the question

  • Thinks the default swallows any exception the lookup raises
  • Believes getattr is only for hacks, never production code
  • Passes a non-string name and expects AttributeError
  • Assumes setattr always succeeds regardless of __slots__
  • Thinks delattr can remove a class attribute through an instance
  • Confuses the getattr builtin with a type's missing-name fallback hook

context