The Built-In getattr()
, hasattr()
, setattr()
, and delattr()
Functions
by Christoph Schiessl on Python
One of Python's, and indeed any object-oriented language's, most fundamental features is the ability to work with instance variables. All Python objects can potentially have "attributes", which is the Python-specific term for "instance variables". Attributes are only useful if they can be read and manipulated. In Python, four atomic operations on attributes form the basis for all other, more complex operations. This article explores these operations and demonstrates how to customize their behavior by implementing or overriding magic methods.
Reading Attributes with getattr()
The default way to read the attributes of an object is the dot notation. So, you would write iron_man.first_name
to read the first_name
attribute from the iron_man
object. This works fine for most use cases, but getattr(object, name)
provides two additional features that can be very handy.
Firstly, you are not limited to Python identifiers. The dot notation works only if the attribute name is a valid Python identifier because you would get a SyntaxError
otherwise.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, first_name: str, last_name: str) -> None:
... self.first_name = first_name
... self.last_name = last_name
...
>>> iron_man = Hero(first_name="Tony", last_name="Stark")
>>> setattr(iron_man, "==>", "rocket") # we have to use setattr() to be able to assign the attribute with this name.
>>> iron_man.first_name, iron_man.last_name
('Tony', 'Stark')
>>> getattr(iron_man, "first_name"), getattr(iron_man, "last_name")
('Tony', 'Stark')
>>> iron_man.==>
File "<stdin>", line 1
iron_man.==>
^^
SyntaxError: invalid syntax
>>> getattr(iron_man, "==>")
'rocket'
Secondly, in addition to the getattr(object, name)
function, there is also a variant ... the getattr(object, name, default)
function with default
parameter, whose value is returned in cases in which object
doesn't have an attribute with the given name
. To achieve the same with the dot notation, you would have to catch the AttributeError
and then return your default value.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, first_name: str, last_name: str) -> None:
... self.first_name = first_name
... self.last_name = last_name
...
>>> iron_man = Hero(first_name="Tony", last_name="Stark")
>>> iron_man.middle_name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Hero' object has no attribute 'middle_name'
>>> getattr(iron_man, "middle_name")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Hero' object has no attribute 'middle_name'
>>> try:
... iron_man.middle_name
... except AttributeError:
... "Edward"
...
'Edward'
>>> getattr(iron_man, "middle_name", "Edward")
'Edward'
I want to be 100% clear about this. The dot notation and getattr()
without a default value can react only in two ways: either a value is returned or an AttributeError
is raised. On the other hand, the getattr()
function with a default value must always return a value and never raise an AttributeError
.
By the way, the name mangling of private attributes still applies. Accessing private attributes from the outside is questionable, but if that's what you want, you are still responsible for mangling the names yourself. Neither the dot notation nor the getattr()
function can help you with that.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, full_name: str) -> None:
... self.__full_name = full_name
...
>>> iron_man = Hero(full_name="Tony Edward Stark")
>>> iron_man.__full_name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Hero' object has no attribute '__full_name'
>>> iron_man._Hero__full_name # using the mangled attribute name
'Tony Edward Stark'
>>> getattr(iron_man, "__full_name")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Hero' object has no attribute '__full_name'
>>> getattr(iron_man, "_Hero__full_name") # using the mangled attribute name
'Tony Edward Stark'
Customizing Attribute Access
There are two magic methods that you can utilize to customize the lookup of attributes. Firstly, there is the more low-level __getattribute__()
method, and secondly, the more high-level __getattr__()
method. When you attempt to access an attribute, Python first calls the object's __getattribute__()
method. All objects, without exception, have this method — there is a default implementation that looks for the attribute in the so-called instance dictionary. If the attribute doesn't appear in the dictionary, then the default implementation raises an AttributeError
. You can override __getattribute__()
to customize its behavior and, for instance, return attributes that don't appear in the instance dictionary.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, first_name: str) -> None:
... self.first_name = first_name
... def __getattribute__(self, name):
... if name == 'last_name':
... return 'Stark'
... return super().__getattribute__(name)
...
>>> iron_man = Hero(first_name="Tony")
>>> iron_man.first_name, iron_man.last_name
('Tony', 'Stark')
>>> getattr(iron_man, "first_name"), getattr(iron_man, "last_name")
('Tony', 'Stark')
When __getattribute__()
raises an AttributeError
, Python tries to call the object's __getattr__()
method. However, not all objects have a __getattr__()
method since there is no default implementation.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, first_name: str, last_name: str) -> None:
... self.first_name = first_name
... self.last_name = last_name
... @property
... def full_name(self) -> str:
... return " ".join([self.first_name, self.middle_name, self.last_name])
... def __getattr__(self, name):
... if name == 'middle_name':
... return "Edward"
... raise AttributeError(f"{type(self).__name__} has no attribute '{name}'")
...
>>> iron_man = Hero(first_name="Tony", last_name="Stark")
>>> iron_man.middle_name, getattr(iron_man, "middle_name")
('Edward', 'Edward')
>>> iron_man.full_name
'Tony Edward Stark'
>>> setattr(iron_man, "middle_name", "top secret") # equivalent to iron_man.__setattr__("middle_name", "top secret")
>>> iron_man.middle_name, getattr(iron_man, "middle_name")
('top secret', 'top secret')
>>> iron_man.full_name
'Tony top secret Stark'
So, which is better to customize attribute lookups? Odds are, you want to implement __getattr__()
and leave __getattribute__()
alone. For instance, if you want to provide default attribute values, then __getattr__()
is the right choice. The only reasonable use case for overriding __getattribute__()
that comes to mind for me is security in the sense that you may want to block reading of certain attributes if they are sensitive. That said, I recommend you keep your hands away from all customized attribute accessing if you can help it because it's definitely not something that enhances readability.
Querying the Existence of Attributes with hasattr()
The hasattr(object, name)
function is the easiest to explain since it simply returns True
if object
has an attribute with the given name
and False
if it does not. Behind the scenes, the behavior is implemented by calling getattr()
with the same parameters so that it returns True
if getattr()
returns some value and False
if it raises an AttributeError
.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, first_name: str) -> None:
... self.first_name = first_name
...
>>> iron_man = Hero(first_name="Tony")
>>> hasattr(iron_man, "first_name")
True
>>> hasattr(iron_man, "last_name")
False
This implementation has the advantage that customizations of getattr()
automatically apply to hasattr()
, too, effectively keeping the two functions in sync. For instance, the implemented __getattr__()
method in the example below makes the computed full_name
attribute visible to both, getattr()
and hasattr()
.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, first_name: str, last_name: str) -> None:
... self.first_name = first_name
... self.last_name = last_name
... def __getattr__(self, name):
... if name == 'full_name':
... return " ".join([self.first_name, self.last_name])
... raise AttributeError(f"{type(self).__name__} has no attribute '{name}'")
...
>>> iron_man = Hero(first_name="Tony", last_name="Stark")
>>> iron_man.full_name
'Tony Stark'
>>> getattr(iron_man, "full_name")
'Tony Stark'
>>> hasattr(iron_man, "full_name")
True
Assigning Attributes with setattr()
Setting attributes with setattr(object, name, value)
or with the dot notation calls the magic __setattr__(self, name, value)
method in the background. All objects have at least the default implementation of this method, which is implemented to add or replace the attribute in the object's instance dictionary.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... pass
...
>>> iron_man = Hero()
>>> iron_man.first_name = "Tony" # equivalent to iron_man.__setattr__("first_name", "Tony")
>>> setattr(iron_man, "last_name", "Stark") # equivalent to iron_man.__setattr__("last_name", "Stark")
>>> iron_man.first_name, iron_man.last_name
('Tony', 'Stark')
>>> getattr(iron_man, "first_name"), getattr(iron_man, "last_name")
('Tony', 'Stark')
>>> hasattr(iron_man, "first_name"), hasattr(iron_man, "last_name")
(True, True)
If you want to, you can override the magic __setattr__(self, name, value)
method to achieve a different behavior. For instance, you could modify the new attribute before calling __setattr__()
of the parent class to set the attribute with the modified value in the instance dictionary.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __setattr__(self, name, value):
... if name == 'last_name':
... value = value.upper()
... super().__setattr__(name, value)
...
>>> iron_man = Hero()
>>> iron_man.first_name = "Tony"
>>> setattr(iron_man, "last_name", "Stark")
>>> iron_man.first_name, iron_man.last_name
('Tony', 'STARK')
Deleting Attributes with delattr()
Last but not least, you can also delete attributes from objects using the del
operator or the equivalent delattr(object, name)
function. Doing so calls the object's magic __delattr__(self, name)
method, which all objects have. The default implementation removes the given attribute from the object's instance dictionary or raises an AttributeError
if the object has no attribute with the given name.
Python 3.12.4 (main, Jun 14 2024, 13:21:39) [GCC 14.1.1 20240522] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Hero:
... def __init__(self, first_name: str, last_name: str) -> None:
... self.first_name = first_name
... self.last_name = last_name
...
>>> iron_man = Hero(first_name="Tony", last_name="Stark")
>>> iron_man.first_name, iron_man.last_name
('Tony', 'Stark')
>>> del iron_man.first_name # equivalent to iron_man.__delattr__("first_name")
>>> hasattr(iron_man, "first_name")
False
>>> delattr(iron_man, "last_name") # equivalent to iron_man.__delattr__("last_name")
>>> iron_man.last_name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Hero' object has no attribute 'last_name'
>>> delattr(iron_man, "last_name") # equivalent to iron_man.__delattr__("last_name")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Hero' object has no attribute 'middle_name'
Finish
Thank you very much for reading, and see you soon! Also, please subscribe to my mailing list below if you've learned something new and want more articles like this one.