Dynamic Attribute Access in Python

Learn dynamic attribute access with getattr(), setattr(), hasattr(), and delattr(). Build object proxies, attribute delegation, and data-driven attribute patterns.

8 min read

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.

pythonpython
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.

texttext
localhost
5432
default_db

The 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.

pythonpython
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.

texttext
30

The 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.

pythonpython
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.

texttext
True
False

The 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.

pythonpython
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.

texttext
True
False

Loading 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.

pythonpython
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.

texttext
True
INFO
3

The 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.

pythonpython
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.

texttext
7
30

The 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.

pythonpython
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.

texttext
Accessed inventory: 100
100
Accessed price: 25
25

For 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.

pythonpython
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.

texttext
DEBUG
light

This 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

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.
RunePowered by Rune AI

Frequently Asked Questions

When should I use getattr() instead of dot notation?

Use getattr() when the attribute name is not known until runtime, such as when it comes from user input, configuration data, or iteration over a list of field names. Use dot notation when the attribute name is fixed and known at write time. getattr() also supports a default value as a third argument, which avoids try/except for missing attributes.

How can I build an object proxy in Python?

An object proxy wraps another object and intercepts attribute access. Implement __getattr__ to forward reads to the wrapped object. Optionally override __setattr__ and __delattr__ for write and delete forwarding. Use __getattribute__ only when you need to intercept every access including special methods. For simple delegation, __getattr__ is usually sufficient.

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.