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#
- Understanding Wrapper Classes and
__getattr__ - How
super()Works in Python - The Problem:
super()Fails to Trigger__getattr__ - Why This Happens: Python’s Attribute Lookup Order
- Troubleshooting Strategies
- Workarounds and Solutions
- Real-World Example: Fixing a Logging Wrapper Subclass
- Conclusion
- 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
LoggingWrappermight log every method call on a database client. - A
ReadOnlyWrappermight 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 objectIf 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__listsSubClass,Parent1,Parent2,object. - super() in Action:
super([type[, object-or-type]])looks up the next class in the MRO oftype(or the MRO of the class ofobject-or-typeif 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 method3. 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:
- Check
obj.__dict__(instance attributes). - Check
obj.__class__.__dict__(class attributes/methods). - Check the
__dict__of all superclasses in the MRO. - 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:
super()returns a proxy object tied to the subclass’s MRO.- Python searches for
attributein the classes listed in the MRO (e.g.,SubWrapper,SimpleWrapper,object), not in the instance’s__dict__or the wrapped object. - If
attributeisn’t found in any of these classes, Python raisesAttributeError—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.attributetriggers__getattr__if the attribute is missing from the instance, class, and superclasses.super().attributeonly checks the class hierarchy (MRO) of the subclass. SinceSimpleWrapper(the parent class) doesn’t have agreetmethod (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 users8. 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.