skip to content

Descriptors and Attribute Access

Three levels of intercepting attribute access: property for one attribute, the descriptor protocol for reusable ones, and __getattr__/__getattribute__ for the whole object. Where 'Python needs no getters' gets real.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

16

What does Python's @property decorator do, and how do you add a setter to it?

level: juniorimportance: must knowfreq 78%

answer

  1. A method that reads like an attribute
  2. One name, up to three functions
  3. The decorator returns a new object
  4. fget, fset, fdel and a docstring
  5. Uniform access, promoted later

basics

~20 s

The @property decorator turns a method into a read-only attribute, so callers write obj.title rather than obj.title(). Decorating a second method of the same name with @title.setter makes assignment work and gives you one place to validate the value.

solid answer

~40 s

`@property` binds the class attribute `title` to a `property` object holding your function in its `fget` slot. Because `property` implements the descriptor protocol, reading `obj.title` calls that function and returns its result — the call moved behind attribute syntax. `@title.setter` is a method on the property object that returns a **new** property carrying the same getter plus your `fset`; the second method must reuse the name `title` so the new object rebinds it. `@title.deleter` does the same for `del obj.title`, and the whole thing is sugar over `title = property(fget, fset, fdel, doc)`. The point is the uniform access principle: ship a plain public attribute, and promote it to a property later — for validation, or a computed value — without touching a single caller.

code

python · 31 lines
python
class Report:
    def __init__(self, rows):
        self._rows = rows
        self._title = "untitled"

    @property
    def row_count(self):
        """Rows collected so far."""
        return len(self._rows)

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        if not value.strip():
            raise ValueError("title must not be blank")
        self._title = value.strip()

    @title.deleter
    def title(self):
        self._title = "untitled"


r = Report(["a", "b"])
r.title = "  Nightly totals  "
print(r.title, r.row_count)   # Nightly totals 2
del r.title
print(r.title)                # untitled
print(type(Report.title).__name__)   # property

go deeper

for a junior

Be ready to write the three-accessor pattern from memory and to say what callers type: obj.title, no parentheses. Know that the setter method must repeat the getter's name and that the stored value lives in a separate private attribute.

for a middle

Explain the mechanics: the decorator binds a property object at class level, .setter returns a new property rather than mutating one, and the read goes through the descriptor protocol. Be able to write the non-decorator property(...) equivalent.

for a senior

Show judgment about what belongs behind a property at all — cheap, repeatable, side-effect-free reads — and be able to argue against reflexive getter/setter pairs in review while pointing at the uniform access principle as the reason Python does not need them.

for a principal

Own the API-evolution angle: a public attribute that can be promoted to a property later is why Python codebases can start simple, and the tradeoff is that attribute access is no longer guaranteed cheap. Decide where your codebase draws that line and write it down.

### `property` is a type, not syntax `property` is a built-in type. Writing `@property` above a method named `title` binds the *class-level* name `title` to a `property` object whose `fget` slot holds your function. From then on, reading `obj.title` on an instance does not find a function in the class — it finds a `property`, and since `property` implements the descriptor protocol, `object.__getattribute__` invokes the property's `__get__`, which calls your function with the instance and hands back the result. The call still happens; it has simply moved behind attribute syntax. Callers write `obj.title`, never `obj.title()`. ### The three accessors A `property` object carries three optional functions — `fget`, `fset`, `fdel` — plus a docstring, and the decorator form fills them one at a time. ```python class Report: @property def title(self): # fget return self._title @title.setter def title(self, value): # fset self._title = value.strip() @title.deleter def title(self): # fdel self._title = "untitled" ``` `@title.setter` is the part that surprises people. `title.setter` is a method **on the property object** that returns a *new* property carrying the original `fget` and the new `fset`; it does not mutate the original. That is why the second method has to reuse the name `title` — the returned object is rebound to that class-level name, replacing the getter-only one. Give the second method a different name and you end up with two separate attributes: a read-only `title` and a useless write-only one. `@title.deleter` behaves identically for `del obj.title`, and `@title.getter` replaces the getter while keeping the other two accessors. Without decorators it is a plain call: `title = property(_get_title, _set_title, _del_title, "doc")`. The decorator form is sugar over exactly that; there is no other machinery. ### The three shapes that cover real code **Read-only / computed.** Only a getter. `row_count` derives from `self._rows`; there is nothing to store and nothing to set. Assignment raises `AttributeError` instead of quietly creating an instance attribute, which is what makes it genuinely read-only rather than merely undocumented. **Validated.** Getter plus setter. The setter is the single funnel every assignment passes through, so invariants — non-blank, in range, normalized, coerced — are enforced in one place, and the value lives in a private backing attribute (`self._title`) that the getter reads. Note the naming discipline: the property owns the public name, the backing attribute takes the underscore. Reusing the same name inside the setter (`self.title = value`) is infinite recursion. **Read-only outside, writable inside.** A public getter and no setter, while the class's own methods assign to `self._title` directly. The property guards the public name, not the object's internals. ### Why Python does not ask for getters up front In languages where turning a public field into an accessor pair is a source-compatibility break, the defensive habit is to write `getTitle()`/`setTitle()` for everything on day one. Python does not need that. Start with a plain public attribute `self.title = title`; if you later need validation, normalization, logging or a derived value, promote the name to a property and **every existing caller keeps working unchanged**. That is the uniform access principle, and it is why asking in review for a getter/setter pair around a field that only stores a value is asking for noise. Add the property when there is a reason, not in advance. ### Details worth carrying into the interview - Accessing the property on the **class** (`Report.title`) returns the `property` object itself, because `__get__` receives `obj=None` and returns `self`. That is exactly what makes `@Base.title.setter` reachable from a subclass body. - The docstring comes from the getter, so `help()` and editors still show it. - A property is one object per class, not per instance, but each read is a Python-level function call and is measurably slower than reading a plain attribute. That only matters in a hot loop. - Properties and `__slots__` coexist only if the names differ: putting `"title"` in `__slots__` *and* defining a property named `title` fails at class creation with `ValueError: 'title' in __slots__ conflicts with class variable`. Slot the backing name `_title` instead. - A property is a promise that reading is cheap and repeatable. Keep I/O, mutation and anything expensive out of a getter — that is what a method is for.

  • What do you get back when you access the property on the class instead of on an instance?
    The `property` object itself. `__get__` is called with `obj=None` and returns `self` rather than invoking the getter, so `Report.title` is a `property` and `Report.title.fget` is the underlying function. That is the hook a subclass uses when it writes `@Base.title.setter` to replace one accessor.
  • How would you write the same property without decorator syntax?
    As a plain call in the class body: `title = property(_get_title, _set_title, _del_title, "the report title")`, with the three functions defined above it under private names. The decorator form is sugar over that constructor — each of `@title.setter`, `@title.getter` and `@title.deleter` just returns a new `property` copying the accessors it is not replacing.
  • Why does assigning to `self.title` inside the setter blow up?
    Because `title` is the property, so the assignment re-enters the same setter and recurses until `RecursionError`. The setter must write to a different name — a private backing attribute such as `self._title`. The property owns the public name; the stored state lives under another one.
  • Should every attribute get a property for safety?
    No. That is the accessor-pair habit imported from languages where promoting a field is a breaking change. In Python a plain attribute can become a property later without changing callers, so a getter/setter that only reads and writes a value adds a call and a maintenance burden for nothing. Add one when there is a real invariant, a computation, or something to hide.

A property is a receptionist at a desk labelled with the attribute's name: visitors just say the name, and whether that fetches a stored file, computes a total or refuses a delivery is the receptionist's business, not theirs.

saying these in an interview costs you the question

  • Says @property makes an attribute private
  • Calls it with parentheses: obj.title()
  • Gives the setter method a different name from the getter
  • Assigns to self.title inside the setter, causing recursion
  • Claims every field should get getters and setters up front
  • Thinks @property stores or caches the computed result

context

open as a page

Why does `self._data = value` inside `__setattr__` recurse forever, and what is the fix?

level: middleimportance: must knowfreq 45%

basics

~20 s

Because an assignment inside setattr is itself an attribute assignment, so it calls setattr again and never terminates, ending in RecursionError. Break the cycle by delegating to object.setattr or super().setattr, or by writing straight into the instance dictionary.

open as a page

How does `__getattribute__` differ from `__getattr__` on a Python class?

level: middleimportance: must knowfreq 55%

basics

~20 s

getattribute is called for every attribute access on an instance and performs the whole lookup itself; getattr is only the fallback the runtime calls when that lookup fails with AttributeError. Overriding getattribute touches every access and every internal read.

open as a page

How does functools.cached_property make every read after the first skip the descriptor?

level: middleimportance: must knowfreq 48%

basics

~20 s

It is a non-data descriptor: the first read runs the function and stores the result in the instance __dict__ under the same attribute name. Later reads find that entry first, so the descriptor never runs again.

open as a page

How do data and non-data descriptors differ in Python's attribute lookup order?

level: middleimportance: must knowfreq 55%

basics

~20 s

A data descriptor defines set or delete and outranks the instance dict; a non-data descriptor defines only get and loses to it. So instance data can shadow a non-data descriptor but never a data one.

open as a page

When does Python call a class's `__getattr__` method on an instance?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Python calls getattr only as a fallback, after the normal attribute search of the instance dictionary and the class hierarchy has already failed. It receives the attribute name as a string and should raise AttributeError for names it cannot supply.

open as a page

How do you invalidate a value cached by functools.cached_property on one instance?

level: juniorimportance: should knowfreq 28%

basics

~20 s

Delete the attribute — del obj.attr — which removes the entry from the instance __dict__ so the next read recomputes. If nothing was cached yet, that raises AttributeError, so guard it or pop from obj.__dict__ with a default.

open as a page

What makes a Python class a descriptor, and what do __get__, __set__ and __delete__ do?

level: juniorimportance: should knowfreq 42%

basics

~20 s

A descriptor is any class that defines get, set or delete and whose instance is stored as a class attribute. Those three methods intercept reading, assigning and deleting that attribute on instances of the owning class.

open as a page

Why does functools.cached_property raise TypeError on a class that defines __slots__?

level: middleimportance: should knowfreq 34%

basics

~10 s

__slots__ removes the per-instance __dict__, and that dictionary is exactly where cached_property writes its result. With nowhere to cache, the first read raises TypeError naming the class and the attribute.

open as a page

How do you override just the setter of a @property inherited from a base class?

level: middleimportance: should knowfreq 40%

basics

~20 s

Reach the base's property object explicitly and decorate with it: @Base.title.setter over a new method named title. That returns a new property keeping the inherited getter and deleter. Redefining the property with a bare @property in the subclass silently drops both.

open as a page

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

level: middleimportance: should knowfreq 55%

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.

open as a page

Why does reaching a plain function through an instance produce a bound method in Python?

level: middleimportance: should knowfreq 40%

basics

~20 s

Function objects implement get, making every function a non-data descriptor. Reaching one through an instance calls that get, which returns a bound method object pairing the function with the instance so the first parameter is supplied automatically.

open as a page

A `__getattr__` proxy in a 6-hour nightly inventory sync grows memory without bound — how do you find and fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Suspect memoisation: because getattr fires only on a miss, the usual speed trick stores each resolved value on the instance, turning every proxy into a permanent cache that pins the fetched data. Confirm with heap snapshots and live-instance counts, then bound it.

open as a page

Why can functools.cached_property run its function twice on Python 3.12 and later?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Python 3.12 removed the lock that cached_property used to hold, so concurrent threads reading the attribute on a cold instance can each run the function. Both write to the instance dict and the last write wins.

open as a page

In a nightly report generator a computed @property re-reads the clock on every access, so two fields in one row disagree — when is a property the wrong tool?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A property promises that reading is cheap, repeatable and side-effect free. Anything volatile, expensive or fallible belongs in a method, where the call site shows it. Freeze the clock once per run and pass that value in.

open as a page

A descriptor instance is shared by every object of its class, so where should per-instance state live?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The descriptor is created once, in the class body, so anything stored on self is shared by all instances. Keep per-instance values in the managed object's own dict, or in a weak-keyed mapping when the class has no dictionary.

open as a page