How do you tell at runtime whether a Python object's attribute lives on the instance or its class?
answer
- The usual accessors answer a different question
- Ask the namespaces, not the object
- One mapping per instance, one per class
- Walk the resolution order to find the owner
- Deleting reveals what was underneath
basics
~10 sCheck ownership rather than value: the name is in vars(obj) if the instance owns it, and in vars of some class in type(obj).mro otherwise. getattr and hasattr answer the lookup, not the origin.
solid answer
~50 s`getattr` and `hasattr` deliberately hide the origin - they report what the lookup returns. To find where a binding lives, ask the namespaces directly: `"name" in vars(obj)` tells you the instance owns it, and scanning `type(obj).__mro__` for the first class whose `vars()` contains the name tells you which class supplies it otherwise. `inspect.getattr_static` fetches the value without triggering the descriptor protocol or `__getattr__`, which matters when a class synthesises attributes dynamically and normal access would lie to you. The confirming experiment is `del obj.name`: it removes only the instance entry, so a shadowed class value reappears, and a second `del` raises `AttributeError` because deletion never reaches the class. In a debugging session that trio - `vars`, an MRO scan, and a `del` - settles almost any "why is this value not what I set" question.
code
python · 17 linesdef origin_of(obj, name):
if name in vars(obj):
return "instance"
for klass in type(obj).__mro__:
if name in vars(klass):
return f"class {klass.__name__}"
return "not stored anywhere"
class Job:
timeout = 120
j = Job()
print(origin_of(j, "timeout")) # class Job
j.timeout = 30
print(origin_of(j, "timeout")) # instance
del j.timeout
print(origin_of(j, "timeout"), j.timeout) # class Job 120go deeper
Know the two basic tools: vars(obj) lists only what the object itself owns, and anything else visible on it is coming from its class. Reach for vars before guessing.
Explain why hasattr and getattr cannot answer the ownership question, and be able to walk type(obj).mro to name the class that supplies a value. Know that del removes only the instance entry.
Show the whole diagnostic loop on a live process: confirm ownership, use a static lookup when attribute access is dynamic or side-effecting, and use del as the decisive experiment for a stale override. Say what you would fix afterwards.
Own the design lesson behind the debugging: class-level defaults with per-instance overrides are a real configuration mechanism with real failure modes, and deciding between them and explicit constructor arguments shapes how surprising your objects are to operate.
### The question the usual tools refuse to answer `obj.name`, `getattr(obj, "name")` and `hasattr(obj, "name")` all run the full lookup and report its *result*. That is what you want in application code and exactly the wrong thing while debugging, because two very different situations - the instance owns a value, or the instance owns nothing and a class supplies one - are indistinguishable from the outside. `dir(obj)` is worse for this purpose: it merges the instance and the whole MRO into one sorted list precisely so that interactive completion looks tidy. ### Asking the namespaces directly The honest tools are the namespaces themselves. * `vars(obj)`, equivalently `obj.__dict__`, is the instance's own mapping and nothing else. `"name" in vars(obj)` is a true/false answer to "does this object own it?" * `vars(SomeClass)` is that one class's mapping, a read-only `mappingproxy`. It does not include inherited names. * `type(obj).__mro__` is the ordered list of classes consulted during a lookup. Walking it and reporting the first class whose `vars()` holds the name tells you which class actually supplies the value - useful in a deep hierarchy where several classes could plausibly define it. A four-line helper that returns `"instance"` or the name of the supplying class answers most attribute mysteries faster than a debugger session, and it is safe to paste into a live process. ### When normal access lies Some classes answer for names that exist in no namespace at all: `__getattr__` is called as a last resort when the ordinary lookup fails, and objects can compute attributes on demand. Against such an object, `hasattr` returning `True` proves nothing about storage. `inspect.getattr_static(obj, "name")` is the tool for this. It looks the name up through the same namespaces but **without** invoking the descriptor protocol or `__getattr__`, and raises `AttributeError` when nothing is genuinely stored. It gives you the stored object rather than the value normal access would compute - which is also why it is the safe choice against an object whose attribute access has side effects, such as a proxy that would trigger a network call on touch. ### The `del` experiment Deletion follows the same asymmetry as assignment: `del obj.name` removes the entry from the instance's own mapping and never touches any class. Two consequences make it a decisive diagnostic. First, if the instance was shadowing a class attribute, deleting the instance entry makes the class value visible again - the attribute does not disappear, it changes value. That is the cleanest possible proof that a class attribute was underneath all along. Second, `del obj.name` on an attribute that only the class provides raises `AttributeError`, because deletion refuses to walk up. So a `del` that raises tells you the instance owned nothing, and a `del` that succeeds and leaves the name readable tells you it was shadowing. This also explains a real recovery move: to reset one object back to a class-wide default that it has overridden, `del obj.name` is exactly right, and it is more honest than assigning the default value again, because it restores the *link* rather than pinning a copy. ### Where this earns its keep The pattern shows up wherever configuration has defaults on the class and overrides on instances. "This job is running with a timeout of 30 even though I set the class default to 120" is answered instantly by `vars(job)` showing a stale override, and fixed by deleting it. The same trio settles the opposite complaint - state that seems shared between objects - because an empty `vars(obj)` where you expected per-object data means the value is coming from the class for every instance. One related check is worth adding to the habit: `"name" in vars(obj)` reads the instance mapping, while `obj.__dict__["name"]` both reads it and bypasses the whole lookup chain, so comparing that against `getattr(obj, "name")` reveals anything unusual happening between the two. ### Recording the finding When the answer is "the instance was shadowing a class default", the fix is rarely just the `del`. Decide whether the default belongs on the class at all, whether the override should have been a constructor argument, and whether anything else set the attribute behind your back. The introspection tells you where the value lives; the design conversation is about where it should live.
- What happens if you call `del obj.name` twice when only the class defines the attribute?The first `del` succeeds only if the instance owned a shadowing entry, and after it the class value is visible again. A `del` against an attribute the instance does not own raises `AttributeError` immediately, because deletion never walks the MRO. So two deletes in a row on a shadowed class attribute give you success then `AttributeError`.
- Why prefer `inspect.getattr_static` over plain `getattr` while debugging?Because it reports what is stored rather than what access computes. It skips the descriptor protocol and `__getattr__`, so a class that synthesises attributes on demand cannot make an absent name look present, and an attribute whose access has side effects - a lazy load, a remote call - is not triggered by your inspection.
- How would you reset one object to the class-wide default it has overridden?`del obj.name`. That removes the instance's shadowing entry and restores the fallback to the class attribute, so the object tracks any later change to the default. Re-assigning the current default value instead pins a copy on the instance, which looks the same today and diverges the moment the class default changes.
saying these in an interview costs you the question
- Uses hasattr to decide where an attribute is stored
- Reads dir(obj) as a list of instance attributes
- Expects del obj.x to remove a class attribute
- Thinks vars(obj) includes inherited names
- Resets an override by re-assigning the default value
- Trusts getattr against an object with dynamic attributes