-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
197: Dynamic Attribute Access with __getattr__ and __setattr__
I've been working on a project where I have to pull a massive set of configuration settings from a JSON file. Right now, the code is littered with things like config['database']['connection']['timeout']. It's clunky, it's hard to read, and honestly, it's a pain to type. I want to be able to just type config.timeout and have Python figure out where that lives.
The frustration of bracket notation
Let's try to build a simple wrapper. I'll start with a class that holds a dictionary, and I'll see if I can make it feel more like a native object. Here is where I'm starting:
class ConfigWrapper:
def __init__(self, data):
self.data = data
settings = ConfigWrapper({'api_key': 'SECRET_123', 'port': 8080})
print(settings.api_key)
If you run this, it crashes immediately with an AttributeError. That makes sense—the ConfigWrapper instance doesn't have an attribute called api_key; it has an attribute called data, which happens to be a dictionary containing that key. I could manually map every key to an attribute in __init__, but that's tedious and fails if the configuration changes dynamically.
Catching the "Missing" attribute
This is where __getattr__ comes in. The trick here is that Python only calls __getattr__ as a last resort. If the attribute exists normally (like self.data), Python uses it. If it doesn't, it asks __getattr__: "Hey, do you know how to handle this?"
Let's adjust the class:
class ConfigWrapper:
def __init__(self, data):
self.data = data
def __getattr__(self, name):
print(f"Looking up {name} in the dictionary...")
if name in self.data:
return self.data[name]
raise AttributeError(f"Config has no setting: {name}")
settings = ConfigWrapper({'api_key': 'SECRET_123', 'port': 8080})
print(settings.api_key) # This works now!
print(settings.data) # This still works and doesn't trigger __getattr__
Notice that when I call settings.data, the print statement inside __getattr__ doesn't fire. Python found data in the instance's actual dictionary, so it never bothered asking the fallback method. But when I ask for api_key, it hits that fallback and pulls the value from the internal dictionary. It's a clean way to create a proxy.
The infinite loop of doom
Now, I want to be able to update these settings. I don't want to do settings.data['port'] = 9000; I want settings.port = 9000. Naturally, I think of __setattr__. But __setattr__ is different from __getattr__: it is called every single time an attribute is assigned, regardless of whether it already exists.
I'll try the obvious approach:
class ConfigWrapper:
def __init__(self, data):
self.data = data
def __setattr__(self, name, value):
print(f"Setting {name} to {value}")
self.data[name] = value
settings = ConfigWrapper({'port': 8080})
settings.port = 9000
If you run this, your program will crash with a RecursionError: maximum recursion depth exceeded. Why? Because inside __setattr__, I'm calling self.data = ... (or accessing self.data). Accessing or setting self.data is itself an attribute operation, which triggers __setattr__, which tries to access self.data, and so on. We've built a loop that never ends.
Breaking the cycle
To fix this, we have to bypass the custom __setattr__ logic when we're doing the actual storage. The most reliable way to do this is to call the base class's version of the method using super(), or by manipulating the instance's __dict__ directly. I prefer super()—it's cleaner.
Here is the polished version that handles both reading and writing dynamically:
class ConfigWrapper:
def __init__(self, data):
# Use super().__setattr__ here to avoid triggering our own __setattr__
super().__setattr__('data', data)
def __getattr__(self, name):
if name in self.data:
return self.data[name]
raise AttributeError(f"Setting {name} not found")
def __setattr__(self, name, value):
if name == 'data':
# Allow the internal dictionary to be overwritten normally
super().__setattr__(name, value)
else:
# Redirect everything else into the dictionary
print(f"Updating config: {name} -> {value}")
self.data[name] = value
settings = ConfigWrapper({'port': 8080})
settings.port = 9000 # Triggers __setattr__, updates dictionary
print(settings.port) # Triggers __getattr__, reads dictionary
By using super().__setattr__, we tell Python: "Just do the standard attribute assignment without calling my custom logic." This allows us to maintain our internal state (the data dict) while providing a sleek, attribute-based API for the user.
📋 Practical Task
Exercise: The Dynamic API Response Proxy
Imagine you are building a tool that consumes a deeply nested API response. Instead of drilling down through dictionaries, you want a proxy class that allows you to access keys as attributes. However, there's a twist: if a key is accessed that doesn't exist, the proxy should return a string "NOT_FOUND" instead of raising an AttributeError.
Your Task:
- Create a class named
ApiResponseProxy. - The class should take a dictionary (the API response) in its
__init__method and store it in an attribute called_payload. - Implement
__getattr__so that if a key exists in_payload, it returns the value. If it doesn't, it returns the string"NOT_FOUND". - Implement
__setattr__so that any attempt to set an attribute (e.g.,proxy.status = "success") actually adds that key-value pair to the_payloaddictionary. Ensure you do not cause aRecursionError.
Test your implementation with this snippet:
resp = ApiResponseProxy({'id': 101, 'user': 'Alice'})
print(resp.user) # Expected: Alice
print(resp.email) # Expected: NOT_FOUND
resp.status = 'OK' # Should update _payload
print(resp._payload) # Expected: {'id': 101, 'user': 'Alice', 'status': 'OK'}There are no comments for now.