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 and setattr when possible

Hopefully you learned something useful, if you have comments or improvements you can connect with me on Twitter!

Discuss on Twitter

Subscribe to my newsletter

What's new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy