Dynamic Attributes in Python
Janne Kemppainen |When you define an object in Python you usually give it some attributes that hold the necessary pieces of information in a place that makes sense. However, Python does not limit the use of attributes to the set that were described at object creation time.
Dynamic attributes are ones that are defined after the object instance has been created. They can be patched elsewhere in the same code base or even come from external data sources. And because functions are also objects you can assign them with custom attributes too.
Nothing beats seeing some examples so let’s cut to the chase.
Normal classes
When you define a class normally there is nothing that prevents you from adding new attributes on the fly as you please. They can also be accessed normally.
class Coordinates:
def __init__(self, x = 0, y = 0):
self.x = x
self.y = y
c = Coordinates(1, 2)
# prints {'x': 1, 'y': 2}
print(c.__dict__)
c.description = "My coordinates"
# prints {'x': 1, 'y': 2, 'description': 'My coordinates'}
print(c.__dict__)
# prints "1, 2: My coordinates"
print(f"{c.x}, {c.y}: {c.description}")
In the above example we used the __dict__
property to list all properties of the object. When we defined the description
property it appeared in the object dictionary just like the ones that were defined in the constructor.
If you’re adding properties on the fly be extra careful when trying to access them elsewhere:
d = Coordinates()
d.description
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# AttributeError: 'Coordinates' object has no attribute 'description'
#
# 'Coordinates' object has no attribute 'description'
If you’re not sure that the object you’re receiving has the attribute you can wrap the call in a try ... except
block.
def print_description(obj):
try:
print(obj.description)
except AttributeError:
print("No description")
Dynamic attributes can be really useful when you don’t necessarily know what the object should contain beforehand. A typical use case could be external data that is used to populate fields on an object. Read the last part of this post to learn more.
Functions
As already mentioned, functions can have attributes too. Take a look at this function that counts the number of words in a string:
def count_words(text):
return len(text.split())
# prints {}
print(count_words.__dict__)
A function has the same __dict__
special property as any other object. Here’s a really theoretical use case for dynamic attributes on a function.
def describe_function(func):
print(f"Function: {func.__name__}")
print_description(func)
describe_function(count_words)
# Function: count_words
# No description
count_words.description = "Counts words in a string"
describe_function(count_words)
# Function: count_words
# Counts words in a string
After setting the description attribute you can call describe_function
with the callable and it will print the name and description of your function. I’m quite sure you can imagine more creative uses for function attributes.
Note that we’ve also used the __name__
property that contains the name of the function. The same property is also available for classes but not for object instances.
The code above is not the optimal solution for the problem and you should actually be using docstrings instead:
def count_words(text):
"""
Counts words in a string
"""
return len(text.split())
def describe_function(func):
print(f"Function: {func.__name__}")
print(func.__doc__.strip() if func.__doc__ else "No description")
describe_function(count_words)
# Function: count_words
# Counts words in a text
The docstrings are available through the __doc__
magic attribute and its value is None
when the docstring is not defined.
Special methods
Another way to access and manipulate attributes is using the getattr
, setattr
and delattr
functions. These let you access attributes whose names are unknown when writing the code.
The getattr
function has two required parameters, an object and the attribute name, and an optional default value. If the default value isn’t provided unknown attributes will raise an AttributeError
just like normal attribute access.
The print_description
example could therefore be written like this:
def print_description(obj):
print(getattr(obj, "description", "No description"))
Now we can omit the exception handling because we have defined a default value.
setattr
is really similar but it has three required parameters, the object, attribute name, and value. So the following two lines are equal:
c.description = "My description"
setattr(c, "description", "My description")
In this case you should always use the first option and go with setattr
only when you need to set an attribute value programmatically for attribute names that you don’t already know. You’re more likely to see setattr
used in library and utility code than in business logic.
An exception to this rule is when you need to use attribute names that are not valid identifiers, for example ones that contain space or start with a digit:
# won't work
c.custom description = "some coordinate"
c.3x = 3 * c.x
# works
setattr(c, "custom description", "some coordinate")
setattr(c, "3x", 3 * c.x)
When you want to get rid of an attribute use the delattr
function. If the attribute doesn’t exist an AttributeError
will be raised.
delattr(c, "x")
delattr(c, "x")
# AttributeError: x
As a bonus, there is also the hasattr
function that can be used to check if an object contains an attribute. Its call signature is hasattr(object, attribute)
and it returns a boolean value.
Conclusion
That was a quick introduction to dynamic attributes in Python. The key takeaways from this post are:
- be aware of
AttributeError
- functions can have attributes too
- consider if the attribute could be included in the object definition
- prefer normal attribute access over
getattr
andsetattr
when possible
Hopefully you learned something useful, if you have comments or improvements you can connect with me on Twitter!
Read next in the Python bites series.
Return Many Values as Attributes in Python
Discuss on Twitter
A quick post about dynamic attributes in Python.https://t.co/3qUg7NYGQH
— Janne Kemppainen (@pakstech) October 20, 2020
Previous post
Chained Comparisons in Python