Data Model
Attribute Access
The starting point is ordinary: __init__ stores one real attribute, the _values backing dictionary. The hooks in the next cells customize lookup and assignment around it.
Source
class Settings:
def __init__(self, values):
self._values = dict(values)
settings = Settings({"theme": "dark"})
print(settings._values)Output
{'theme': 'dark'}__getattr__ runs only for missing attributes, so it can provide fallback lookup.
Source
class Settings:
def __init__(self, values):
self._values = dict(values)
def __getattr__(self, name):
try:
return self._values[name]
except KeyError as error:
raise AttributeError(name) from error
settings = Settings({"theme": "dark"})
print(settings.theme)Output
dark__setattr__ intercepts every assignment, including the ones in __init__. Underscore names are stored as real attributes through object.__setattr__, which avoids recursing through your own hook; public names go to the backing dictionary.
Source
class Settings:
def __init__(self, values):
self._values = dict(values)
def __setattr__(self, name, value):
if name.startswith("_"):
object.__setattr__(self, name, value)
else:
self._values[name] = value
settings = Settings({"theme": "dark"})
settings.volume = 7
print(settings._values["volume"])Output
7Notes
__getattr__is narrower than__getattribute__because it handles only missing attributes.__setattr__affects every assignment on the instance.- Use
propertyor descriptors when the behavior is attached to a known attribute name.
See also
- prerequisite: Properties
- related: Descriptors
- related: Special Methods
- related: Bound and Unbound Methods
Run the complete example
Expected output
dark
7
Execution time appears here after you run the example.