29.6 Properties vs __getattr__: When Each Is Appropriate
In object-oriented Python, the mechanisms for controlling attribute access are powerful but distinct. Two primary tools for this are properties and the __getattr__ method. Understanding their fundamental differences—specifically, the distinction between data access via the descriptor protocol and via the lookup chain—is crucial for employing them correctly.
Properties are a declarative tool built on Python’s descriptor protocol. They allow you to define getter, setter, and deleter methods for a specific, predefined attribute, transforming what looks like simple attribute access into method calls. This is ideal when you have a known attribute whose value you need to compute, validate, or otherwise control at the point of access.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Get the temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set the temperature in Celsius with validation."""
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible.")
self._celsius = value
@property
def fahrenheit(self):
"""Compute and return the temperature in Fahrenheit."""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set the temperature by assigning a Fahrenheit value."""
self.celsius = (value - 32) * 5/9
# Usage
temp = Temperature(25)
print(temp.celsius) # 25 - Direct access, but through getter
print(temp.fahrenheit) # 77.0 - Computed on access
temp.fahrenheit = 32 # Triggers the fahrenheit setter
print(temp.celsius) # 0.0 - Value was validated and set via celsius.setter
Properties are appropriate when the set of attributes is fixed and known at design time. They provide a clean, intuitive interface that encapsulates implementation details, such as the internal storage in _celsius and the computation for fahrenheit. The key advantage is that existing code using obj.attribute syntax doesn’t need to change when a property is introduced.
The __getattr__ Method and Dynamic Attribute Handling
In contrast, the __getattr__ method is an imperative and dynamic fallback mechanism. It is only invoked by the Python interpreter after the normal attribute lookup process—checking the instance dictionary, the class dictionary, and base classes—has failed to find the attribute. This makes it perfect for handling access to attributes that are not predefined.
class DynamicConfig:
"""A class that simulates a configuration object with fallback defaults."""
_defaults = {'theme': 'dark', 'language': 'en', 'items_per_page': 25}
def __init__(self, user_settings=None):
# User-provided settings override defaults
self._user_settings = user_settings or {}
def __getattr__(self, name):
"""Called only if the attribute wasn't found by normal lookup."""
if name in self._defaults:
# Check user settings first, then fall back to default
return self._user_settings.get(name, self._defaults[name])
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
# Usage
config = DynamicConfig({'theme': 'light'})
print(config.theme) # 'light' - from user_settings
print(config.language) # 'en' - from _defaults via __getattr__
print(config.items_per_page) # 25 - from _defaults via __getattr__
# This will correctly raise an AttributeError
# print(config.unknown_setting)
__getattr__ is the right choice for implementing delegation, lazy loading, or providing a flexible interface where the available attributes might be dynamic or extensible, such as with configuration objects, proxies, or wrappers around other objects.
Key Differences and Common Pitfalls
A critical pitfall is confusing __getattr__ with __getattribute__. The latter is called for every attribute access, unconditionally, which makes it far more powerful but also much easier to accidentally create infinite recursion (e.g., by trying to access self.attr inside the method). You must use super().__getattribute__() or object.__getattribute__(self, ...) inside __getattribute__ to avoid this. __getattr__ is simpler and safer for most fallback use cases.
Another common mistake in __getattr__ is forgetting to raise an AttributeError for genuinely missing attributes. Failing to do so breaks the expected behavior of built-in functions like hasattr().
Performance is another consideration. Property access involves a method call, but the attribute name is resolved at compile time. __getattr__ involves the entire attribute lookup chain failing before being invoked, making it inherently slower. It should not be used for attributes that are accessed frequently in performance-critical code.
Best Practices and When to Choose Which
Use Properties when:
- You are dealing with a fixed, known set of attributes.
- You need to encapsulate validation logic or computation upon getting/setting a value.
- You want to maintain a simple, attribute-like interface while adding behavior.
- You need to control deletion with
@deleter.
Use __getattr__ when:
- The set of attributes is dynamic, unpredictable, or extensible (e.g., based on external data).
- You are implementing a proxy or wrapper that delegates most attributes to another object.
- You want to provide fallback values or lazy loading for a large number of potential attributes.
- The attributes are truly optional and not part of the core interface.
For the most robust design, often the best solution is a combination: use properties for the core, well-defined attributes of your object and employ __getattr__ as a fallback for more dynamic or optional behavior. This hybrid approach leverages the strengths of both mechanisms.