skip to content

Why does assigning to a getter-only @property raise AttributeError instead of setting an instance attribute?

level: middleimportance: should knowfreq 55%

answer

  1. The class is consulted before the instance
  2. It defines __set__ even when empty
  3. Data descriptors outrank stored values
  4. Forcing it into __dict__ changes nothing
  5. The 3.11 message names class and attribute

basics

~20 s

A property is a data descriptor: it defines set as well as get, and attribute lookup gives data descriptors on the class priority over the instance dict. With no setter function, the property's set raises AttributeError instead of storing anything.

solid answer

~40 s

`property` always defines `__set__` and `__delete__`, even when you supplied only a getter — that makes it a **data descriptor**. `object.__setattr__` looks the name up on `type(obj)` and its MRO first; finding a data descriptor, it delegates to that descriptor's `__set__` rather than writing into `obj.__dict__`. With `fset` empty, `__set__` raises `AttributeError`; since 3.11 the message names both sides — `property 'row_count' of 'Report' object has no setter` — where older versions said only `can't set attribute`. The symmetry matters on reads too: even if you force a value in with `obj.__dict__['row_count'] = 99`, the read still returns the computed value, because the data descriptor outranks the instance dict on the way out as well. `del obj.row_count` fails the same way with `has no deleter`.

code

pycon · 15 lines
pycon
>>> class Report:
...     def __init__(self, rows):
...         self._rows = rows
...     @property
...     def row_count(self):
...         return len(self._rows)
...
>>> r = Report(["a", "b"])
>>> r.row_count = 99
Traceback (most recent call last):
    ...
AttributeError: property 'row_count' of 'Report' object has no setter
>>> r.__dict__["row_count"] = 99
>>> r.row_count
2

go deeper

for a junior

Recall the practical rule: a @property with only a getter is read-only, and assigning to it raises AttributeError rather than quietly storing a value. Read the 3.11+ message — it names the property and the class for you.

for a middle

Explain the mechanism: object.__setattr__ searches the type's MRO before the instance dict, a property defines __set__ even with no setter, and that __set__ is what raises. Be ready to say why a forced __dict__ entry is still ignored on read.

for a senior

Use it in design arguments: because the descriptor cannot be shadowed per instance, a validated setter really is the single funnel and the invariant holds. Also know the escape hatches — writing the backing attribute, or replacing the property on the class in a test — and their cost.

for a principal

Frame read-only as a signalling and invariant tool, not a security boundary; nothing stops a caller reaching the private backing name. Decide where the codebase relies on enforced invariants versus documented convention, and keep that line consistent.

### What actually runs on `obj.x = 5` Assignment to an attribute is not a dictionary write; it is a call to `type(obj).__setattr__(obj, "x", 5)`, which for ordinary classes is `object.__setattr__`. That implementation does **not** start at the instance dictionary. It first walks `type(obj).__mro__` looking for `x`. If it finds something there that defines `__set__` (or `__delete__`) as well as `__get__` — a **data descriptor** — it hands the assignment to that object's `__set__` and stops. Only when the class-level lookup finds nothing, or finds something that is not a data descriptor, does the value land in `obj.__dict__`. `property` is a data descriptor unconditionally. It implements `__get__`, `__set__` and `__delete__` in C, and it implements them whether or not you passed an `fset` or `fdel`. A getter-only property therefore still owns assignment to its name — and its `__set__`, finding `fset` empty, raises `AttributeError`. That is the whole answer: the exception is deliberate, raised by the property, not a side effect of something being missing. ### The message, and what version says what ```pycon >>> r.row_count = 99 AttributeError: property 'row_count' of 'Report' object has no setter ``` That wording arrived in Python 3.11; before it, all you got was `AttributeError: can't set attribute`, with no hint about which attribute or which class — a genuinely annoying thing to debug in a class with a dozen properties. `del r.row_count` produces the parallel `has no deleter`. If you see the old terse message in a traceback from production, you are on 3.10 or earlier. ### The read side is the half people forget Because `property` is a *data* descriptor, it also wins on the way out. `object.__getattribute__` checks the type's MRO first; a data descriptor found there is invoked immediately and the instance dictionary is never consulted. So this is legal and completely useless: ```python r.__dict__["row_count"] = 99 # stored, and permanently ignored r.row_count # still 2 — the property answers ``` The value really is sitting in `r.__dict__` — `vars(r)` will show it — but nothing will ever read it through the attribute. This is the crisp difference from a *non-data* descriptor, which yields to the instance dict and so can be shadowed by a stored value. A property never can be. (Being shadowable is a whole mechanism of its own and belongs to the caching-descriptor discussion, not here.) ### Why this is a feature Read-only in Python is rarely about defence against a malicious caller — anyone can reach `r._rows` or reassign `Report.row_count` on the class. It is about **invariants that hold by construction**. `row_count` is defined as `len(self._rows)`; there is no state that could be set independently, so an assignment is by definition a bug in the caller, and failing loudly at the assignment is far better than silently creating a stale instance attribute that some other code path later reads. The alternative — plain attributes — has exactly that failure mode: a typo'd or obsolete assignment succeeds and rots quietly. The same reasoning applies to a validated property: because the class-level descriptor cannot be bypassed by an instance-dict write, the setter really is the only funnel, and the invariant it enforces really does hold for every value that arrived through the public name. ### The common escapes, and what they cost - **Write the backing attribute.** `r._rows.append(x)` or `r._title = "x"` from inside the class. The property guards the public name only; internal code is trusted. - **Replace the descriptor on the class.** `Report.row_count = 42` swaps the property out for every instance. Legal, occasionally used in tests, and a genuine source of spooky action at a distance. - **Per-instance override.** Not available: you cannot give one instance a different `row_count` by assignment, because the class-level data descriptor intercepts it. You would have to give that instance a different class. ### Interview shape The question usually arrives as a bug report — *"I set `obj.x = 5` and it raised, and when I forced it into `__dict__` the read still gave the old value"* — and what is being tested is whether you know that attribute access consults the type before the instance, and that `property` occupies both directions. Say "data descriptor, class before instance, `__set__` raises when `fset` is `None`" and you have answered it.

  • If a forced write into the instance __dict__ is ignored on read, why does the write itself succeed?
    Because `obj.__dict__["x"] = 99` is a plain dictionary write — it never goes through `object.__setattr__`, so no descriptor is consulted. The entry genuinely exists and `vars(obj)` shows it. Only attribute *access* is intercepted, and there the class-level data descriptor is checked first, so the stored entry is unreachable through `obj.x`.
  • How do you expose a value as read-only to callers while the class still updates it?
    Define a getter-only property over a private backing attribute, and have the class's own methods assign to that backing name. `obj.total` is unsettable from outside, while `self._total = ...` inside the class works normally. Python's read-only is about signalling and invariants, not enforcement — a determined caller can always touch `_total` or replace the property on the class.
  • Why does `del obj.x` fail on a property that has a setter but no deleter?
    `property.__delete__` is defined unconditionally, just like `__set__`, so `del` is routed to the property rather than removing an instance-dict entry. With `fdel` unset it raises `AttributeError: property 'x' of 'C' object has no deleter` (3.11+ wording). Supply `@x.deleter` if deletion should mean something — usually resetting the backing attribute rather than removing it.

saying these in an interview costs you the question

  • Thinks the assignment silently creates an instance attribute
  • Says the property is only consulted when the instance dict lacks the name
  • Believes __slots__ is what blocks the assignment
  • Claims you must write __setattr__ to make an attribute read-only
  • Thinks the AttributeError comes from the getter failing
  • Says forcing the value into __dict__ makes the read return it

context