Checking for attributes
MP 128: Sometimes a try-except block isn't the best approach for handling missing attributes.
In Python it's often reasonable to try doing something, and then respond appropriately if that action doesn't work. This is the "ask for forgiveness rather than asking for permission" approach to error handling.
Try-except blocks are great, and you should use them without much concern if they address the issue you're facing. However, they're not the only way to handle situations that might generate errors. In this post I'll share an example of a try-except block that I recently replaced with a better approach using the hasattr()
function. But first I'll share an example that shows how quickly software systems can develop enough complexity that something like missing attributes need to be taken into consideration.
A weather example
Let's use weather as an example, and let's go a bit beyond a single-program example. Imagine the following chain of events:
- A programmer writes code for modeling weather.
- Someone makes a weather observation.
- A news outlet writes a widget for displaying current weather data.
This is quite realistic. This plays out more generally when one person writes a library, someone else writes code that uses that library to store some data, and a third person writes code that processes that data for public consumption.
In both of these examples, each user has to consider the possibility that someone before them in the chain has changed what they do.
Modeling weather
Here's a short class modeling a weather observation:
from dataclasses import dataclass @dataclass class WeatherObservation: precip: float temp: float
This is a dataclass, where an observer can record two data points for any given observation: a precipitation amount, and a temperature.
Observing weather
Here's a bit of code written by a scientist making a single weather observation:
from wx_model import WeatherObservation wx_obs = WeatherObservation(2.5, 1.5)
The observer has imported WeatherObservation
, and made one instance representing a single observation.
Reporting weather
Finally, someone writes a short program to provide a summary of the most recently recorded observation:
import wx_observer def show_summary(obs): print("\nObservation summary:") print(f" Precipitation: {obs.precip}cm") print(f" Temp: {obs.temp}C") show_summary(wx_observer.wx_obs)
The reporter imports the observer's code, and has a function to summarize the data from a single observation. They call this function with the most recent observation.
Here's the output:
$ python wx_reporter.py Observation summary: Precipitation: 2.5cm Temp: 1.5C
This is all fantastic. Everyone's doing their own work, and end users get a nice summary of current conditions.
Multiple observers, and evolving code
Now imagine multiple people are reporting observations, and the reporter wants to gather as many observations from the area as possible. The problem is, different observers are using different versions of wx_model.py.
Here's the latest version of wx_model.py, which includes a field for wind speed:
@dataclass class WeatherObservation: precip: float temp: float wind: float
Some people are using the older version of the model, which is limited to recording precip
and temp
. But some are using the newer version that also records wind speed. How does the reporter handle the situation where some observers will report wind speed observations and some will not?
Here's an approach using exception handling:
import wx_observer import wx_observer_wind def show_summary(obs): print("\nObservation summary:") print(f" Precipitation: {obs.precip}cm") print(f" Temp: {obs.temp}C") try: print(f" Wind: {obs.wind}kph") except AttributeError: pass show_summary(wx_observer.wx_obs) show_summary(wx_observer_wind.wx_obs)
The line reporting wind speed is wrapped in a try-except block. If the wind speed is available, it's reported. If it's not available, the program doesn't crash.
Here's the output:
$ python wx_reporter.py Observation summary: Precipitation: 2.5cm Temp: 1.5C Observation summary: Precipitation: 0.5cm Temp: 23.0C Wind: 15.0kph
This is great! But there's another way to handle the possibility of missing data.
Using hasattr()
The hasattr()
function checks whether a Python object has a specific attribute. Here's how that simplifies the show_summary()
function:
def show_summary(obs): print("\nObservation summary:") print(f" Precipitation: {obs.precip}cm") print(f" Temp: {obs.temp}C") if hasattr(obs, "wind"): print(f" Wind: {obs.wind}kph")
If obs
has the attribute wind
, we print that data as part of the observation. If obs
doesn't have a wind
attribute, we don't do anything.
When to use hasattr()
So when do you use hasattr()
instead of a try-except block? First of all, there's no real issue with using exception handling in Python over if statements. You don't usually need to check something with an if statement before doing it in a try-except block. (This is known as a guard clause.)
I like to use hasattr()
when the except
block just has a pass
statement. If we're only doing something when an attribute is available, there's no need for the two parts of the try-except block. I also like to use it if I'm not actually doing anything in the try
block, except seeing whether an attribute is available or not.
A real-world example
Here's an example from django-simple-deploy. When a plugin for a specific platform claims to support fully automated deployments, it needs to provide a message telling the user exactly what actions will be taken on their behalf. Here's a snippet of the original code that validates plugins:
try: plugin_config.confirm_automate_all_msg except AttributeError: msg = "Doesn't provide a confirmation message." raise SimpleDeployCommandError(msg)
This block tries to access the attribute confirm_automate_all_msg
. If an AttributeError
is raised, the program exits with an appropriate message.
It never felt good to just access an attribute without actually using it in any way, as the single line in the try
block does in this example. I did a bit of reading, and realized I could just check whether the confirm_automate_all_msg
attribute is available or not using hasattr()
. Here's the refactored code:
if not hasattr(plugin_config, "confirm_automate_all_msg"): msg = "Doesn't provide a confirmation message." raise SimpleDeployCommandError(msg)
This change is more significant than just reducing the number of lines of code. The revised code more accurately reflects what I'm trying to do here. I don't want to use the confirmation message at this point in the codebase; I just want to make sure it's available before doing any other work. The intention of this code block is much clearer than the original try-except block.
Conclusions
Python's try-except blocks provide really flexible ways to handle errors, including the possibility that something your code depends on might not exist. But there are other ways of handling incorrect or missing data. If the reasoning represented by your current approach doesn't align clearly with the way you're thinking about your code, consider whether there's another way to handle the situation you're modeling. Specifically, if your try-except block is built around the possibility that an attribute may not exist, see if using hasattr()
leads to a more intuitive solution.
Also, keep in mind that if you're dealing with growing complexity because you're trying to support multiple versions of a library, you can always tell your users they need to be using more recent versions of that library. In this example, the reporter could specify a minimum version of the weather model that's supported, and remove the conditional code that checks whether wind speed is included. It's a tradeoff though; you replace the conditional code around wind speed with a conditional check around the version of the underlying weather model that's used.
Resources
You can find the code from this post in the mostly_python GitHub repository.