Python: debug unraisable exceptions with Rich

Take this Python class:
class Widget:
def __del__(self):
1 / 0
It implements a __del__ method to customize when the object is deleted. The division by zero there will raise a ZeroDivisionError, a quick way to fail.
But let’s try triggering that error by adding the following:
w = Widget()
del w
print("Continued...")
When you run it, you’ll see:
$ python example.py
Exception ignored in: <function Widget.__del__ at 0x104a31d00>
Traceback (most recent call last):
File "/.../example.py", line 3, in __del__
1 / 0
~~^~~
ZeroDivisionError: division by zero
Continued...
The exception is displayed with a strange “Exception ignored in” prefix. Then, the program continues as if no error occurred, printing “Continued”.
So, what’s going on?
Well, the __del__ method is a special case. Python does not allow any exceptions within this method to propagate. Instead, it only logs them as “unraisable”.
Python has this behaviour because an object’s deletion can happen at any time. Without an explicit del statement, the garbage collector deletes objects when they’re no longer referenced. Since the garbage collector can run whenever, even during shutdown, raising an exception there is unsafe.
Exceptions in other places are also unraisable, for example, when a generator catches the GeneratorExit exception. Python doesn’t have an official list of cases, but you can see them all by searching the source for PyErr_WriteUnraisable, which is the C function to log an unraisable exception.
Python logs unraisable exceptions through sys.unraisablehook(). You can swap this hook to change what is displayed. For example, pytest uses it to accumulate unraisable exceptions in its “warnings summary”:
$ pytest test_example.py
========================= test session starts =========================
...
collected 1 item
example.py . [100%]
========================== warnings summary ===========================
example.py::test_widget
/.../unraisableexception.py:85: PytestUnraisableExceptionWarning: Exception ignored in: <function Widget.__del__ at 0x1052cc180>
Traceback (most recent call last):
File "/.../test_example.py", line 3, in __del__
1 / 0
~~^~~
ZeroDivisionError: division by zero
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================== 1 passed, 1 warning in 0.00s =====================
Debugging unraisable exceptions can be tricky. The stack trace is minimal, stopping with the method that Python called, such as __del__.
One tool that I’ve found useful is Rich, a terminal colourization library with a module for printing tracebacks. This can show the values of local variables for each frame in the stack. Here’s how you can define a custom sys.unraisablehook() that displays the traceback with Rich:
import sys
import rich
from rich.traceback import Traceback
def unraisablehook(unraisable):
rich.print(
Traceback.from_exception(
unraisable.exc_type,
unraisable.exc_value,
unraisable.exc_traceback,
show_locals=True,
)
)
sys.unraisablehook = unraisablehook
Let’s use it with a slightly modified example, with x and y local variables:
class Widget:
def __del__(self):
x = 1
y = 0
x / y
w = Widget()
del w
print("Continued...")
Running this now, we get:

The “locals” box shows pretty-printed values of the variables in that frame. In a more complex example, this extra information can be valuable.
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts:
- Python: spy for changes with
sys.monitoring - Python: profile total memory allocated with tracemalloc
- Python: Fail in three characters with
1/0
Tags: python