Skip to main content

__getattr__ and __setattr__: Dynamic Attribute Access in Python

Advanced25 min5 exercises100 XP
0/5 exercises

Most of the time, Python attributes are straightforward. You set obj.x = 5, and later obj.x gives you 5. But what if you want attributes that don't actually exist — attributes that are computed on the fly, or that trigger side effects when accessed?

Python gives you full control over attribute access through special methods: __getattr__, __getattribute__, __setattr__, and __delattr__. These let you intercept every read, write, and delete operation on any attribute.

Think of these methods as a security desk at a building entrance. Every time someone tries to enter (read), drop something off (write), or remove something (delete), the desk can check, log, redirect, or deny the request.

What Is __getattr__ and When Does It Run?

__getattr__ is Python's fallback for attribute access. It's only called when the normal lookup fails — when the attribute doesn't exist in the instance dict, the class, or any parent class.

__getattr__ is a fallback
Loading editor...

This is the key thing to remember: __getattr__ is a fallback. It only triggers for missing attributes. Attributes that exist in __dict__ or on the class are found through normal lookup and never hit __getattr__.

What Is the Difference Between __getattr__ and __getattribute__?

This is one of the most confusing distinctions in Python:

  • __getattr__ — called only when the attribute is not found through normal means
  • __getattribute__ — called on every single attribute access, even for attributes that exist
  • __getattribute__ is the nuclear option. It intercepts everything.

    __getattribute__ intercepts everything
    Loading editor...

    Here's how the full lookup chain works:

  • __getattribute__ is called (always)
  • It checks data descriptors on the class
  • It checks instance.__dict__
  • It checks non-data descriptors and class attributes
  • If nothing is found, __getattr__ is called as a last resort
  • How Do You Intercept Setting and Deleting Attributes?

    __setattr__ is called every time an attribute is set — including inside __init__. This means you need to be careful to avoid infinite recursion:

    Auditing all attribute changes
    Loading editor...

    The pattern is the same as __getattribute__: inside __setattr__, you can't write self.x = value normally (that would call __setattr__ again). Use super().__setattr__(name, value) to actually store the attribute.

    Wrong: Causes infinite recursion
    class Bad:
        def __setattr__(self, name, value):
            self.log = []  # Recursion!
            self.__dict__[name] = value
    Right: Use super() or __dict__
    class Good:
        def __setattr__(self, name, value):
            # Option 1: super()
            super().__setattr__(name, value)
            # Option 2: direct __dict__
            # self.__dict__[name] = value

    What Are Real-World Uses for Dynamic Attributes?

    Dynamic attribute access enables powerful patterns that would be verbose or impossible otherwise. Here are some of the most useful ones.

    Dot-notation dictionary access
    Loading editor...

    Notice the use of super().__setattr__('_data', data) in __init__. We can't use self._data = data because that would trigger our custom __setattr__, which tries to access self._data — but it doesn't exist yet!

    Deprecation warnings for old attribute names
    Loading editor...

    What Is Python's Complete Attribute Lookup Chain?

    Here's the complete picture of what happens when you access obj.x:

  • type(obj).__getattribute__(obj, 'x') is called
  • Check for a data descriptor named x on type(obj) and its MRO
  • Check obj.__dict__['x'] (the instance dictionary)
  • Check for a non-data descriptor or class variable named x on type(obj) and its MRO
  • If nothing found, call obj.__getattr__('x') (if defined)
  • Raise AttributeError
  • And for setting obj.x = value:

  • type(obj).__setattr__(obj, 'x', value) is called
  • Check for a data descriptor named x on type(obj) — if found, call its __set__
  • Otherwise, store value in obj.__dict__['x']

  • Practice Exercises

    Build a Default Dictionary Object
    Write Code

    Create a class DefaultObj that returns a default value for any attribute that doesn't exist. The constructor takes the default value.

    Regular attributes should work normally. Only missing attributes should return the default.

    obj = DefaultObj('N/A')
    obj.name = 'Alice'
    print(obj.name)      # Alice
    print(obj.missing)   # N/A
    print(obj.whatever)  # N/A
    Loading editor...
    Predict the Attribute Access
    Predict Output

    What does the following code print?

    class Magic:
        x = 10
        
        def __getattr__(self, name):
            return f'fallback:{name}'
    
    obj = Magic()
    obj.y = 20
    print(obj.x)
    print(obj.y)
    print(obj.z)

    Remember: __getattr__ is only called when normal lookup fails.

    Loading editor...
    Build a Read-Only Object
    Write Code

    Create a class Frozen that allows attributes to be set in __init__ but raises AttributeError with the message 'Cannot modify frozen object' if anyone tries to set an attribute after initialization.

    Use a flag _frozen to track whether initialization is complete.

    obj = Frozen(name='Alice', age=30)
    print(obj.name)  # Alice
    print(obj.age)   # 30
    Loading editor...
    Fix the Recursive __setattr__
    Fix the Bug

    This code tries to log attribute changes, but it crashes with infinite recursion. Fix it so it prints:

    Setting name = Alice
    Setting age = 30
    Alice is 30
    Loading editor...
    Build a Proxy Object
    Write Code

    Create a Proxy class that wraps another object and forwards all attribute access to it. The proxy should also count how many times attributes were accessed.

    The constructor takes the wrapped object. Add a method get_access_count() that returns the total number of attribute accesses on the wrapped object.

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    p = Person('Alice', 30)
    proxy = Proxy(p)
    print(proxy.name)          # Alice
    print(proxy.age)           # 30
    print(proxy.name)          # Alice
    print(proxy.get_access_count())  # 3
    Loading editor...