Customize Python Objects with Magic Methods

Learn to control attribute access on Python objects with __getattr__, __setattr__, __delattr__, and __getattribute__, plus practical customization patterns.

8 min read

Customizing Python objects with magic methods means controlling what happens when someone reads, writes, or deletes an attribute on your objects. The language provides four hooks for this: the getattr method for fallback access, the getattribute method for intercepting every access, the setattr method for controlling writes, and the delattr method for controlling deletions.

The article on special methods in Python covered the foundational dunder methods like the string representation methods and comparison operators. This article focuses on the attribute access family, which gives you the power to build objects that validate data on assignment, compute values on demand, and protect critical state.

These hooks are the foundation of many Python features you already use. The property decorator is built on top of them, and ORMs like SQLAlchemy and Django use them to turn attribute access into database queries. Proxy objects and remote procedure call libraries use them to forward attribute access across network boundaries.

The normal attribute lookup process

Before customizing attribute access, it helps to understand what Python does by default. When you write obj.name, Python follows a specific lookup order.

First, Python looks for a data descriptor on the type, which wins if the type defines both a get method and a set or delete method. Next, Python checks the instance dictionary for a matching key. If the key exists, the value is returned directly.

If the key is not in the instance dictionary, Python looks for a non-data descriptor or a class attribute on the type. If nothing is found at any level, Python calls the getattr method on the object as a last resort before raising AttributeError.

Understanding this lookup chain helps you predict which customization method will fire for a given access pattern.

The getattr method: handling missing attributes

The getattr method is called only when normal attribute lookup fails. It is the fallback handler, the last chance before Python raises an AttributeError.

A common use case is providing default values without storing them explicitly. Instead of checking for every possible key and returning a default, you define getattr once.

pythonpython
class DefaultDict:
    def __init__(self):
        self._data = {}
 
    def __getattr__(self, name):
        if name.startswith("_"):
            raise AttributeError(name)
        return self._data.get(name, 0)
 
    def __setattr__(self, name, value):
        if name == "_data":
            super().__setattr__(name, value)
        else:
            self._data[name] = value
 
d = DefaultDict()
d.apples = 5
print(d.apples)
print(d.oranges)

The getattr method returns zero for any key that has not been explicitly set. The setattr method routes attribute writes into the internal dictionary.

texttext
5
0

The underscores-prefix check prevents infinite recursion. Without it, accessing self._data inside getattr would call getattr again because _data has not been set yet at that point in init.

The getattribute method: intercepting every access

The getattribute method is more powerful and more dangerous. Python calls it for every attribute access on the object, even for attributes that exist. This is the method you override when you need to log, transform, or redirect every single dot access.

Because this method fires unconditionally, you must be careful to call the superclass version for any attribute you do not intend to intercept. Forgetting to do so causes infinite recursion.

pythonpython
class LoggedAccess:
    def __init__(self, value):
        self._value = value
 
    def __getattribute__(self, name):
        import logging
        logging.info("Accessing %s", name)
        return super().__getattribute__(name)
 
obj = LoggedAccess(42)
print(obj._value)

The method logs every access and then delegates to the default implementation. The super call is essential. Without it, accessing self._value inside getattribute would call getattribute again forever.

texttext
42

Use getattribute sparingly. It adds overhead to every attribute access and makes the code harder to reason about. Most use cases are better served by getattr or properties.

The setattr method: controlling attribute writes

The setattr method fires every time someone assigns a value to an attribute on the object. It receives the attribute name and the new value. You can validate, transform, or reject the assignment before storing it.

The critical rule is that you must call the superclass setattr to actually store the value. Assigning directly with self.name = value inside setattr causes infinite recursion.

pythonpython
class PositivePoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
    def __setattr__(self, name, value):
        if value < 0:
            raise ValueError(f"{name} must be non-negative, got {value}")
        super().__setattr__(name, value)
 
p = PositivePoint(3, 5)
p.x = 10
print(p.x)
 
try:
    p.y = -2
except ValueError as e:
    print(e)

The setattr method validates every assignment as it arrives. Negative values are rejected before they ever reach the instance dictionary, keeping the object in a valid state at all times.

texttext
10
y must be non-negative, got -2

Notice that init assigns with self.x = x rather than calling super().setattr directly. This routes the constructor's own values through the same validation, so a negative x or y is rejected immediately instead of slipping into the object unchecked.

Building a validated data container

Combining these methods lets you build objects that enforce data integrity at the attribute level. Here is a simple validated record that checks types on every field assignment.

pythonpython
class ValidatedRecord:
    _fields = {}
 
    def __init__(self, **kwargs):
        for name, value in kwargs.items():
            setattr(self, name, value)
 
    def __setattr__(self, name, value):
        expected = self._fields.get(name)
        if expected and not isinstance(value, expected):
            raise TypeError(
                f"{name} expected {expected.__name__}, got {type(value).__name__}"
            )
        super().__setattr__(name, value)
 
class Person(ValidatedRecord):
    _fields = {"name": str, "age": int}
 
p = Person(name="Maya", age=30)
print(p.name, p.age)
p.age = 31
print(p.age)

The base class checks every assignment against the field type map. Subclasses define the expected types by setting the _fields dictionary.

texttext
Maya 30
31

Trying to assign a string to the age field would raise a TypeError before the assignment completes. This pattern gives you compile-time-like type checking at runtime without external libraries.

When to use each customization method

Use getattr when you want to provide computed defaults or delegate attribute access to another object. It only fires when the attribute is missing, so it has no performance impact on normal access.

Use getattribute only when you genuinely need to intercept every single attribute access, such as for debugging proxies or access logging. Its performance cost and complexity are justified only in rare cases.

Use setattr when you need to validate or transform every assignment. For single-attribute validation, the property decorator is usually cleaner. For cross-attribute constraints or type enforcement across many attributes, setattr is the right tool.

The article on dynamic attribute access in Python covers patterns like object proxies and attribute delegation that build on these foundations.

Rune AI

Rune AI

Key Insights

  • getattr handles attribute access when normal lookup fails; getattribute intercepts every access.
  • setattr controls all attribute assignments and must call super().setattr to actually store values.
  • delattr controls attribute deletion and pairs with setattr for symmetric access control.
  • Always call the superclass method inside custom access methods to avoid infinite recursion.
  • Use properties for simple per-attribute validation; reach for setattr only when you need cross-attribute control.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between __getattr__ and __getattribute__?

__getattribute__ is called for every attribute access on an object, regardless of whether the attribute exists. __getattr__ is called only when the normal attribute lookup fails, making it a fallback handler. Use __getattr__ for default values and delegation. Use __getattribute__ only when you need to intercept every single access.

When should I customize __setattr__?

Customize __setattr__ when you need to validate, transform, or log every attribute assignment on an object. Common use cases include enforcing type constraints, preventing accidental overwrites of critical attributes, and implementing observer patterns where attribute changes trigger callbacks.

Conclusion

Attribute access customization gives you fine-grained control over how Python objects store and retrieve data. Start with properties for simple validation, use getattr for fallback behaviour, and reach for setattr only when you need to intercept every assignment. Each tool serves a distinct purpose, and using the right one keeps your code clear and predictable.