Managed Attributes

This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

A managed attribute looks like a plain field from the outside (obj.value) but runs code on access, assignment, or deletion. Python gives you three layers to build one: @property for the common case, the descriptor protocol it is built on, and the _getattr_/_getattribute_ hooks for whole-object attribute interception.

@property: Computed and Validated Attributes

@property turns a method into an attribute-like accessor, so callers write obj.value instead of obj.get_value() while the class still runs code on every read. Reference: the property() built-in.

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @property
    def area(self):
        return 3.14159 * self._radius ** 2   # computed on every read, never stored


c = Circle(2)
print(c.radius)    # 2            -- looks like a plain attribute
print(c.area)       # 12.56636    -- computed, not stored

Setters and Deleters

A bare @property is read-only — assigning to it raises AttributeError. @<name>.setter and @<name>.deleter add the missing operations, letting a setter validate before storing:

class Circle:
    def __init__(self, radius):
        self.radius = radius          # goes through the setter below, validated from construction on

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("radius must be positive")
        self._radius = value

    @radius.deleter
    def radius(self):
        del self._radius


c = Circle(3)
c.radius = 5          # runs the setter's validation
# c.radius = -1        # raises ValueError: radius must be positive
del c.radius           # runs the deleter

The setter and deleter are attached with @radius.setter/@radius.deleter — decorating a second and third method that reuses the property’s name, which is why all three defer to a single private self._radius for storage.

Descriptors: the Protocol Behind @property

@property is not special-cased in the interpreter — it is an ordinary class implementing the descriptor protocol: any object defining _get_, _set_, or _delete_ and assigned as a class attribute intercepts attribute access on every instance. Full walkthrough, including how property itself is implemented in pure Python: the Descriptor HowTo Guide.

class PositiveNumber:
    def __set_name__(self, owner, name):
        self._name = "_" + name        # remembers which attribute it was assigned to

    def __get__(self, instance, owner):
        if instance is None:
            return self                # accessed on the class itself, e.g. Circle.radius
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError(f"{self._name[1:]} must be positive")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]


class Circle:
    radius = PositiveNumber()          # descriptor instance, assigned as a CLASS attribute

    def __init__(self, radius):
        self.radius = radius           # triggers PositiveNumber.__set__


c = Circle(3)
print(c.radius)   # 3   -- triggers PositiveNumber.__get__
c.radius = 5       # triggers PositiveNumber.__set__
# c.radius = -1     # raises ValueError: radius must be positive

A descriptor defining _set_ or _delete_ is a data descriptor and takes priority over an instance’s own dict; one defining only _get_ is a non-data descriptor and an instance attribute of the same name wins instead. @property always defines all three methods, so it is always a data descriptor — which is why assigning self.radius = …​ inside _init_ still goes through its setter rather than silently creating a shadowing instance attribute.

_getattr_ vs. _getattribute_

Both hooks intercept obj.name, but at different points in the lookup, per the data model reference:

  • _getattribute_ runs for every attribute access, unconditionally — normal lookup (instance dict, then class, then descriptors) is whatever the default implementation does internally. Overriding it means reimplementing that lookup yourself for every attribute, including ones you do not care about.

  • _getattr_ runs only as a fallback, after normal lookup has already failed with AttributeError — it never sees an attribute that was found normally, so it only needs to handle the missing case.

Validating with _getattr_ (the common case)

Use _getattr_ to compute or validate attributes that do not otherwise exist, without touching lookup for anything that does:

class Config:
    def __init__(self):
        self.debug = True                 # a real attribute; __getattr__ never sees it

    def __getattr__(self, name):
        if name.startswith("_"):
            raise AttributeError(name)
        return f"<no setting '{name}'>"    # fallback for any other missing attribute


cfg = Config()
print(cfg.debug)        # True                          -- normal lookup, __getattr__ not called
print(cfg.timeout)       # <no setting 'timeout'>        -- lookup failed, __getattr__ called

Validating with _getattribute_ (intercepting everything)

Use _getattribute_ only when every access — existing attributes included — must be checked, and always delegate to the base implementation to keep normal lookup working:

class Locked:
    def __init__(self):
        self.value = 42

    def __getattribute__(self, name):
        if name == "value":
            print(f"reading {name!r}")
        return super().__getattribute__(name)   # required: performs the actual lookup


obj = Locked()
print(obj.value)   # prints "reading 'value'", then 42 -- runs even though 'value' exists

Forgetting super().getattribute(name) and instead writing self.name inside _getattribute_ recurses infinitely, since reading self.name calls _getattribute_ again.

See Also

  • Classes and Objects — classes, instances, and dict, the foundation @property and descriptors build on.

  • Decorators and Metaclasses — @property is itself a decorator; metaclasses are the other main hook into attribute and class creation.