Python Errors Done Right

An opinionated approach to structure your try-except code

Ilia Zaitsev
Jun 23 · 5 min read

Exceptions mechanism is widely adopted among modern programming languages, and Python is not an exception. (Pun intended!) Though this topic could seem obvious, like wrap your code blocks with try-catch clauses, and that’s all, there are some minor but important details, and taking them into account should make your code a bit cleaner.

In this post, I’m going through some guidelines on how to structure error processing in Python that I derived from my personal experience.

Image for post
Image for post
Photo by Sarah Kilian on Unsplash

Use Meaningful Classes

It is very tempting to write the following code, and I’ve seen it several times in projects where I was a collaborator.

if something_is_wrong():
raise Exception('error!')

It is better to use a more specific exception class instead. It sounds like common wisdom, but still, every once in a while, you can see this approach of raising the most generic exception instead of being specific.

Of course, the first reason to use more specific exceptions is to make possible writing multi-branch error handlers as the following snippet shows.

try:
function()
except ValueError:
process_value_error()
except AttributeError:
process_attribute_error()
...
except Exception: # everything non-specific gets there
failure()

The second reason is more of a semantical point of view. The generic exception class doesn’t give you any hint into what went wrong. If you pick ValueError or TypeError instead, it immediately gives a bit more informative feedback to the reader even before reading the error message itself.

In case if your package includes some complex logic and many sub-modules, you could even inherit from the base class to create a dedicated error with some additional functionality or information. In most cases, however, it is perfectly fine to go with built-in types, and even some large and complex Python libraries do so.

Include Traceback

Consider the following snippet. It catches the error and prints it to the standard output.

def some_complex_arithmetics(x, y):
return x / y
x, y = 1, 0
try:
result = some_complex_arithmetics(x, y)
except ZeroDivisionError as e:
print(e)

Here is what you’ll see.

division by zero

Looks good, right? Somewhat, yes. But what if you what to dig deeper and see what exactly happened there? Like, on what line of code and what function if there are many of them calling each other? This information is usually shown when you’re debugging in IDE or run your script, and it fails with an unexpected error. Whenever an unhandled error is raised, the full traceback is printed.

However, if you catch the exception explicitly in your code, these valuable lines are missing. Let’s get them back! For this purpose, use built-in traceback a module that allows you to "manually" access the information about the error.

import tracebackdef some_complex_arithmetics(x, y):
return delegate(x, y) + delegate(y, x)
def delegate(x, y):
return x / y
x, y = 1, 0try:
result = some_complex_arithmetics(x, y)
except ZeroDivisionError as e:
lines = traceback.format_tb(e.__traceback__)
for line in lines:
print(line)
print(f'{type(e).__name__}: {e}')

This time, you’ll see a full traceback.

File "errors_simple.py", line 14, in <module>
result = some_complex_arithmetics(x, y)
File "errors_simple.py", line 5, in some_complex_arithmetics
return delegate(x, y) + delegate(y, x)
File "errors_simple.py", line 8, in delegate
return x / y
ZeroDivisionError: division by zero

The module is especially useful when writing custom logging functionality as soon as you can enrich and process the error’s output however you like.

Use Decorators to Catch Them All

Now let’s pretend you want to ensure that no error escape unhandled. For this purpose, you wrap functions with the most generic exception clause.

# one module
try:
result = first_function()
except Exception as e:
print(e)
sys.exit(1)
...# another module
try:
result = second_function()
except Exception as e:
print(e)
sys.exit(1)

It works, but what if your package has many publicly exposed functions that should be guarded? Writing the same boilerplate code again and again takes time and is not easy to maintain. A better option would be to go with a more generic solution that would allow making your functions “bullet-proof”. Python decorators sound like a good choice for this.

In one of my projects, I’ve used something similar to the snippet below to ensure that no error happens without proper logging.

Again, as in the previous section, this decorator does something very similar to what you see when executing a script, and it fails with some unhandled errors. Though here you have better control over errors formatting and logging while keeping the code clean and reusable.

Hide Hard-Coded Strings

Including too many magic values into your program is ubiquitously known as a bad practice. Though still, you can often see something like this in some big projects.

def product(a: float, b: float) -> float:
if np.isnan(a):
raise ValueError('computation error: argument "a" is NaN')
if np.isnan(b):
raise ValueError('computation error: argument "b" is NaN')
return a * b

But what if you need to change the error message a bit? And what if the same error is raised in many places? Sure enough, one can go with a search-replace technique, but it is not something easily maintainable. A better option is to keep these error message strings under dedicated enumeration.

Note how we use the same error formatting twice within themultiply function. Otherwise, we would need to copy-paste the same message twice.

See a great example from the spacy library where all error messages are structured nicely and at a single place. It looks like an overhead for the smaller programs, but whenever you work on large projects with many developers/users, it becomes a very valuable strategy.

Conclusion

I hope this short write-up was helpful to you. One could say that these recipes are rather obvious and straightforward. But sometimes even minor tips could make a big difference and help to make your programs a bit more resilient and clear for you and your fellow developers.

Interested in Python language? Can’t live without Machine Learning? Have you read everything else on the Internet?

Then probably you would be interested in my blog where I am talking about various programming topics and provide links to textbooks and guides I’ve found interesting.

The Startup

Medium's largest active publication, followed by +733K people. Follow to join our community.

Ilia Zaitsev

Written by

Software Developer & AI Enthusiast. Working with Machine Learning, Data Science, and Data Analytics. Writing posts every once in a while.

The Startup

Medium's largest active publication, followed by +733K people. Follow to join our community.

Ilia Zaitsev

Written by

Software Developer & AI Enthusiast. Working with Machine Learning, Data Science, and Data Analytics. Writing posts every once in a while.

The Startup

Medium's largest active publication, followed by +733K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store