Dynamic attribute access in Python means reading, writing, checking, and deleting object attributes using names that are determined at runtime rather than written directly in code. The built-in functions getattr, setattr, hasattr, and delattr are the entry points for this pattern.
The article on customizing Python objects with magic methods covered the internal hooks that fire during attribute access. This article focuses on the caller side: how to work with attributes when the names come from data, configuration, or user input rather than being hardcoded.
Dynamic access is essential for data mapping, configuration loading, serialization, and building flexible wrappers and proxies. Without it, you would need long chains of if-elif blocks or repetitive code for every possible attribute name.
The four built-in dynamic access functions
Python provides four built-in functions for working with attributes by name. Each one accepts the object and the attribute name as a string.
The getattr function reads an attribute. If the attribute does not exist and you provide a third argument, that default value is returned instead of raising an AttributeError.
class Config:
host = "localhost"
port = 5432
cfg = Config()
print(getattr(cfg, "host"))
print(getattr(cfg, "port"))
print(getattr(cfg, "database", "default_db"))The third argument to getattr provides a safe fallback when the requested attribute does not exist. This approach is cleaner than wrapping every attribute access in a try-except block for AttributeError.
localhost
5432
default_dbThe setattr function writes an attribute using a runtime-determined name. It is equivalent to the dot assignment syntax, but the attribute name comes from a string variable rather than being written directly in code.
setattr(cfg, "timeout", 30)
print(cfg.timeout)The timeout attribute is created dynamically on the object. Any valid Python string can become an attribute name at runtime through the setattr function.
30The hasattr function checks whether a named attribute exists on the given object. It returns a boolean value without raising an exception, making it safe to call before attempting access.
print(hasattr(cfg, "host"))
print(hasattr(cfg, "username"))This is the clean way to check for attribute existence before access. The function internally attempts the access and catches any resulting AttributeError.
True
FalseThe delattr function removes an attribute from an object by its string name. It works the same way as the del statement, but the attribute name is determined at runtime.
setattr(cfg, "temp", "remove_me")
print(hasattr(cfg, "temp"))
delattr(cfg, "temp")
print(hasattr(cfg, "temp"))The attribute is created with setattr and then removed by name with delattr. After deletion, hasattr correctly reports that the attribute no longer exists on the object.
True
FalseLoading configuration from a dictionary
A common use case for dynamic access is mapping dictionary keys to object attributes. Instead of manually assigning each key, you iterate and use setattr.
class Settings:
pass
data = {"debug": True, "log_level": "INFO", "max_retries": 3}
settings = Settings()
for key, value in data.items():
setattr(settings, key, value)
print(settings.debug)
print(settings.log_level)
print(settings.max_retries)Every key in the dictionary becomes an attribute on the settings object. Adding a new key to the dictionary automatically creates the corresponding attribute.
True
INFO
3The reverse direction, converting an object to a dictionary, uses getattr with a list of field names or introspection through the dir built-in.
Building an object proxy
An object proxy wraps another object and forwards attribute access to it. The proxy can add logging, access control, or lazy initialization without modifying the wrapped object.
class Proxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name):
return getattr(self._target, name)
class Calculator:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
calc = Calculator()
proxy = Proxy(calc)
print(proxy.add(3, 4))
print(proxy.multiply(5, 6))The proxy defines getattr to forward any missing attribute to the target object. The add and multiply calls succeed as if they were made directly on the Calculator instance.
7
30The underscore-prefixed _target attribute is not forwarded because the underscore check in getattr prevents it. This avoids infinite recursion when the proxy's own attributes are accessed.
Filtering attribute access through a proxy
A proxy can do more than forward. It can filter, transform, or block specific attributes. Here is a proxy that logs every access to the target object.
class LoggingProxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name):
value = getattr(self._target, name)
print(f"Accessed {name}: {value}")
return value
class Store:
inventory = 100
price = 25
store = Store()
proxy = LoggingProxy(store)
print(proxy.inventory)
print(proxy.price)Every attribute access prints a message showing the attribute name and its value. This pattern is useful for debugging, auditing, and observing interactions with objects you cannot modify.
Accessed inventory: 100
100
Accessed price: 25
25For production use, replace the print statement with structured logging. The proxy pattern is non-invasive because the target object requires no changes.
Delegating attribute access to a fallback chain
Sometimes an object should try multiple sources for an attribute. Dynamic access lets you build a delegation chain that checks each source in order.
class SourceChain:
def __init__(self, *sources):
self._sources = sources
def __getattr__(self, name):
for source in self._sources:
if hasattr(source, name):
return getattr(source, name)
raise AttributeError(name)
defaults = Config()
defaults.theme = "light"
env = Config()
env.log_level = "DEBUG"
chain = SourceChain(env, defaults)
print(chain.log_level)
print(chain.theme)The chain checks env first, then defaults. Attributes defined in multiple sources resolve to the first source that has them.
DEBUG
lightThis pattern mirrors how configuration systems merge environment variables, config files, and hardcoded defaults. Dynamic access makes the chain generic rather than requiring explicit code for each attribute.
The standard library already solves this exact problem for dictionaries with collections.ChainMap, which searches multiple mappings in order without any custom class. Reach for the pattern above only when your sources are objects rather than dictionaries.
The article on attribute lookup in Python covers the internal lookup chain that getattr, setattr, and hasattr all build on.
Rune AI
Key Insights
- getattr(obj, name, default) accesses attributes dynamically with an optional fallback value.
- setattr(obj, name, value) and delattr(obj, name) set and delete attributes by runtime name.
- hasattr(obj, name) checks existence by attempting access and catching AttributeError.
- Object proxies forward attribute access to a wrapped object using getattr.
- Combine dynamic access with data-driven field lists for configuration mapping and serialization.
Frequently Asked Questions
When should I use getattr() instead of dot notation?
How can I build an object proxy in Python?
Conclusion
Dynamic attribute access turns attribute names from static identifiers into runtime values. The built-in getattr, setattr, hasattr, and delattr functions let you write code that adapts to data-driven field names. Combined with the customization hooks from earlier articles, these tools let you build flexible proxies, data mappers, and configuration systems that feel natural in Python.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.