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() 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.

Ready to Learn More Web Development?

Join my Mailing List to receive one article per week.


I send one email per week on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


The Built-In sum() Function

In this article, we explore Python's built-in sum() function, its parameters, and some extreme use cases it wasn't even designed for.

By Christoph Schiessl on Python

The Built-In min() and max() Functions

Python's min() and max() functions can take an iterable or multiple positional parameters, and support a key parameter for custom comparisons.

By Christoph Schiessl on Python

The Built-In sorted() Function

Python's sorted() function makes sorting easy and performant, guaranteeing stability and allowing descending order and custom mapping.

By Christoph Schiessl on Python

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast web applications.


I'm available for hire as a freelance web developer, so you can take advantage of the more than a decade of experience I have collected working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm also an avid writer and educator who takes pride in breaking down technical concepts into the simplest possible terms.