Why Doesn't Python's `super()` Call `__getattr__` in Wrapper Class Subclasses? Troubleshooting Attribute Access

Python’s object-oriented paradigm offers powerful tools for code reuse, including inheritance and dynamic attribute handling. Wrapper classes (e.g., proxies, decorators, or adapters) are a common pattern to extend or modify the behavior of existing objects, often using __getattr__ to forward attribute access to an underlying "wrapped" object. However, developers often encounter confusion when subclassing these wrappers and using super(): unexpectedly, super() calls may fail to trigger __getattr__, leading to AttributeError even when the wrapped object has the attribute.

This blog demystifies this behavior by exploring Python’s attribute lookup mechanics, the role of super(), and why __getattr__ and super() don’t always play well together in wrapper subclasses. We’ll walk through troubleshooting steps, workarounds, and real-world examples to help you resolve these issues.

Table of Contents#

  1. Understanding Wrapper Classes and __getattr__
  2. How super() Works in Python
  3. The Problem: super() Fails to Trigger __getattr__
  4. Why This Happens: Python’s Attribute Lookup Order
  5. Troubleshooting Strategies
  6. Workarounds and Solutions
  7. Real-World Example: Fixing a Logging Wrapper Subclass
  8. Conclusion
  9. References

1. Understanding Wrapper Classes and __getattr__#

What Are Wrapper Classes?#

Wrapper classes (or "proxies") wrap an internal object (the "wrapped object") to intercept or extend its behavior. Common use cases include logging method calls, adding caching, or validating inputs. For example:

  • A LoggingWrapper might log every method call on a database client.
  • A ReadOnlyWrapper might block write operations on a mutable object.

The Role of __getattr__#

To forward attribute access to the wrapped object, wrappers often override __getattr__. From Python’s docs:

__getattr__(self, name) is called when the default attribute access fails (i.e., the attribute isn’t found in the instance’s __dict__, its class’s __dict__, or the superclasses’ __dict__).

Key Behavior: __getattr__ is a fallback: it only runs if the attribute cannot be found via normal lookup. It’s not called for attributes that exist in the wrapper class itself or its superclasses.

Example Wrapper with __getattr__:

class SimpleWrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped  # The object being wrapped
 
    def __getattr__(self, name):
        """Forward missing attributes to the wrapped object."""
        print(f"Forwarding '{name}' to wrapped object")
        return getattr(self.wrapped, name)  # Delegate to wrapped object

If we wrap an object with a greet method, SimpleWrapper forwards the call:

class Greeting:
    def greet(self):
        return "Hello, World!"
 
wrapper = SimpleWrapper(Greeting())
print(wrapper.greet())  # Output: Forwarding 'greet' to wrapped object; Hello, World!

2. How super() Works in Python#

super() is used to access methods or attributes from a parent class in an inheritance hierarchy. It returns a "proxy object" that delegates calls to the appropriate class in the method resolution order (MRO) of the subclass.

Key Concepts:#

  • MRO: The order in which Python searches for classes when resolving attributes/methods. For a class SubClass(Parent1, Parent2), SubClass.__mro__ lists SubClass, Parent1, Parent2, object.
  • super() in Action: super([type[, object-or-type]]) looks up the next class in the MRO of type (or the MRO of the class of object-or-type if omitted) and delegates the attribute/method call to it.

Example: Basic super() Usage

class Parent:
    def method(self):
        print("Parent method")
 
class Child(Parent):
    def method(self):
        super().method()  # Calls Parent.method()
        print("Child method")
 
Child().method()
# Output:
# Parent method
# Child method

3. The Problem: super() Fails to Trigger __getattr__#

The confusion arises when subclassing a wrapper and using super() to call a method that should be forwarded via __getattr__. Let’s extend our earlier example to see this:

Scenario:#

We create a SubWrapper subclass of SimpleWrapper and try to call a method via super(), expecting SimpleWrapper’s __getattr__ to forward it to the wrapped object.

class SubWrapper(SimpleWrapper):
    def call_wrapped_method(self):
        # Attempt to call 'greet' via super()
        return super().greet()  # ❌ Unexpected AttributeError!
 
# Test it
sub_wrapper = SubWrapper(Greeting())
sub_wrapper.call_wrapped_method()  # AttributeError: 'super' object has no attribute 'greet'

Why the Error?
We expected super().greet() to trigger SimpleWrapper’s __getattr__, which forwards to Greeting().greet(). Instead, Python raises AttributeError. Why?

4. Why This Happens: Python’s Attribute Lookup Mechanism#

To understand why super() doesn’t trigger __getattr__, we need to unpack Python’s attribute lookup steps and how super() interacts with them.

Step 1: Normal Attribute Lookup (Without super())#

When you access obj.attribute, Python follows this order:

  1. Check obj.__dict__ (instance attributes).
  2. Check obj.__class__.__dict__ (class attributes/methods).
  3. Check the __dict__ of all superclasses in the MRO.
  4. If not found, call obj.__getattr__(attribute) (if defined).

Step 2: super() Attribute Lookup#

When you call super().attribute, Python’s lookup is class-centric, not instance-centric:

  1. super() returns a proxy object tied to the subclass’s MRO.
  2. Python searches for attribute in the classes listed in the MRO (e.g., SubWrapper, SimpleWrapper, object), not in the instance’s __dict__ or the wrapped object.
  3. If attribute isn’t found in any of these classes, Python raises AttributeError—it does not invoke __getattr__ on the instance, because __getattr__ is an instance method triggered only after instance-level lookup fails.

The Critical Distinction#

  • obj.attribute triggers __getattr__ if the attribute is missing from the instance, class, and superclasses.
  • super().attribute only checks the class hierarchy (MRO) of the subclass. Since SimpleWrapper (the parent class) doesn’t have a greet method (it relies on __getattr__ to forward), super().greet() fails.

5. Troubleshooting Strategies#

If super() isn’t triggering __getattr__, use these steps to diagnose the issue:

1. Inspect the MRO#

Check the subclass’s MRO to see which classes super() is searching. Use SubWrapper.__mro__:

print(SubWrapper.__mro__)
# Output: (<class '__main__.SubWrapper'>, <class '__main__.SimpleWrapper'>, <class 'object'>)

Here, super() in SubWrapper will search SimpleWrapper and object for greet—neither has it, so AttributeError.

2. Verify __getattr__ Is a Fallback#

Ensure __getattr__ is only called when the attribute is missing. If the wrapper class or its superclasses define the attribute, __getattr__ won’t run. For example:

class SimpleWrapper:
    def greet(self):  # If wrapper defines 'greet', __getattr__ is never called for 'greet'
        return "Wrapper's greet"
 
# Now, even direct access would use the wrapper's 'greet', not the wrapped object's.

3. Check if super() Targets the Wrapper Class#

super() in SubWrapper refers to SimpleWrapper, not the wrapped object. To confirm, print the super proxy’s class:

print(super(SubWrapper, sub_wrapper))  # <super: <class 'SubWrapper'>, <SubWrapper object>>
# This proxy looks in SubWrapper's MRO (SimpleWrapper, object), not the wrapped Greeting.

6. Workarounds and Solutions#

To resolve the super() + __getattr__ conflict, we need to adjust our approach to attribute forwarding in subclasses. Here are proven solutions:

Solution 1: Access the Wrapped Object Directly#

Instead of using super(), explicitly forward the call to the wrapped object via self.wrapped. This bypasses the class-centric super() lookup and ensures __getattr__ isn’t needed for subclass calls.

class SubWrapper(SimpleWrapper):
    def call_wrapped_method(self):
        return self.wrapped.greet()  # ✅ Directly access the wrapped object
 
sub_wrapper = SubWrapper(Greeting())
print(sub_wrapper.call_wrapped_method())  # Output: Hello, World!

Solution 2: Override __getattribute__ (Use with Caution)#

__getattribute__ is called unconditionally for every attribute access (unlike __getattr__, which is a fallback). Overriding it in the wrapper allows intercepting even super() calls, but requires careful handling to avoid infinite recursion.

class SimpleWrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped
 
    def __getattribute__(self, name):
        """Unconditionally intercept attribute access."""
        if name == 'wrapped':  # Avoid infinite recursion when accessing self.wrapped
            return object.__getattribute__(self, name)
        # Forward to wrapped object if attribute not found in wrapper
        try:
            return object.__getattribute__(self, name)
        except AttributeError:
            print(f"Forwarding '{name}' via __getattribute__")
            return getattr(self.wrapped, name)
 
# Now, super() in SubWrapper will trigger __getattribute__
class SubWrapper(SimpleWrapper):
    def call_wrapped_method(self):
        return super().greet()  # ✅ Now works!
 
sub_wrapper = SubWrapper(Greeting())
sub_wrapper.call_wrapped_method()  # Output: Forwarding 'greet' via __getattribute__; Hello, World!

Warning: __getattribute__ is powerful but risky. Always delegate to object.__getattribute__(self, name) for attributes you don’t want to override (like wrapped).

Solution 3: Explicitly Define Forwarded Methods in the Wrapper#

If the wrapped object has known methods, define them in the wrapper class to avoid relying on __getattr__ for critical paths. Subclasses can then use super() safely.

class SimpleWrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped
 
    # Explicitly forward 'greet' instead of relying on __getattr__
    def greet(self):
        return self.wrapped.greet()
 
class SubWrapper(SimpleWrapper):
    def call_wrapped_method(self):
        return super().greet()  # ✅ Now works, since 'greet' is defined in SimpleWrapper
 
sub_wrapper = SubWrapper(Greeting())
sub_wrapper.call_wrapped_method()  # Output: Hello, World!

7. Real-World Example: Fixing a Logging Wrapper Subclass#

Let’s apply these solutions to a practical scenario: a LoggingWrapper that logs database queries, and a CachingWrapper subclass that adds caching.

Problematic Code:#

class DatabaseClient:
    def query(self, sql):
        print(f"Executing: {sql}")
        return f"Result for {sql}"
 
class LoggingWrapper:
    def __init__(self, db_client):
        self.db_client = db_client  # Wrapped database client
 
    def __getattr__(self, name):
        """Log method calls and forward to db_client."""
        def wrapper(*args, **kwargs):
            print(f"Calling {name}({args}, {kwargs})")
            return getattr(self.db_client, name)(*args, **kwargs)
        return wrapper
 
class CachingWrapper(LoggingWrapper):
    def __init__(self, db_client):
        super().__init__(db_client)
        self.cache = {}
 
    def query(self, sql):
        if sql in self.cache:
            return self.cache[sql]
        # Attempt to call the original query via super()
        result = super().query(sql)  # ❌ AttributeError: 'super' object has no attribute 'query'
        self.cache[sql] = result
        return result
 
# Test
db = DatabaseClient()
caching_db = CachingWrapper(db)
caching_db.query("SELECT * FROM users")  # AttributeError!

Fixed Code (Using Solution 1: Direct Wrapped Access)#

class CachingWrapper(LoggingWrapper):
    def __init__(self, db_client):
        super().__init__(db_client)
        self.cache = {}
 
    def query(self, sql):
        if sql in self.cache:
            return self.cache[sql]
        # Directly call the wrapped db_client's query method
        result = self.db_client.query(sql)  # ✅ Bypasses super() lookup
        self.cache[sql] = result
        return result
 
# Now it works!
caching_db = CachingWrapper(db)
caching_db.query("SELECT * FROM users")
# Output:
# Executing: SELECT * FROM users
# Result for SELECT * FROM users

8. Conclusion#

The confusion between super() and __getattr__ in wrapper subclasses stems from Python’s attribute lookup rules:

  • __getattr__ is an instance-level fallback triggered only when an attribute isn’t found in the instance, class, or superclasses.
  • super() performs a class-level lookup in the subclass’s MRO, bypassing the instance’s __getattr__.

To resolve issues:

  • Access the wrapped object directly (e.g., self.wrapped.method()).
  • Override __getattribute__ for unconditional interception (use cautiously).
  • Explicitly define methods in the wrapper to avoid relying on __getattr__.

By understanding Python’s MRO and attribute lookup, you can build robust wrapper hierarchies that work seamlessly with inheritance.

9. References#