That time I had 2200 exceptions in my HelloWorld script

April 6, 2020
Ceesjan,
Chief Technology Officer

“You must be out of your mind, of course you don’t want that”, I told the developer. “Yes, yes I do”, he replied. This argument is why I decided to hack the CPython codebase to find out precisely how many Exceptions are getting thrown in a simple Python script.

To take a few steps back: I was doing a pair-programming session with a developer, and off the cuff he asked me how to see all exceptions being thrown. I pointed him to the checkbox at the bottom of his editor. “There”, I said, “just enable that and you’re good to go”.

VSCode’s Debugging Exceptions checkboxes

“No, that will only show me uncaught Exceptions and Exceptions I raise myself. I’m interested in all of the exceptions, thrown by any piece of code, even if the Exception is being handled.”

“That doesn’t make sense”, I said. “Exceptions are everywhere. We’re talking about Python here, Exceptions are used all the time for perfectly normal flow control. It’s like the air around you.”

“I understand that, but still I want to see all exceptions that get thrown.”

“Why?”

“Well, because in the past I’ve had to debug C# programs that behaved in inexplicable ways, and every once in a while I managed to fix them by inspecting all Exceptions. I want to know how to do this with Python.”

At this point I realized the developer, who is a heavy-weight programmer well versed in C++ and C#, lacked a fundamental experience using Python. Sure, he had read plenty of Python scripts and even made a bunch of his own. He knew the syntax, he understood and accepted the indentation rules and shared plenty of opinions on Python. But the whole EAFP principle hadn’t clicked yet. It’s like tasting food or perceiving colors: you can read up on them for years, but without experiencing them first-hand any discussions you’ll have about these topics are bereft of a fundamental quality.

Seeing how continuing the discussion wouldn’t get me anywhere I said I’d come back to him about this later, finished the pair-programming session and retreated to my office, ready to find some approach to show how his question didn’t make sense.

I would never be able to convince him of the foolishness of his request by using my words. The only way forward was for him to experience it some way. The fastest way to do this was to give him precisely what he asked for, so he could realize that’s not what he wanted. Thus, I set out to somehow let Python show me *all* of the exceptions.

After looking around for a while and trying some things out I settled on hacking the default Python interpreter, CPython, to print all exceptions to stdout. The CPython source code was very easy to understand and within no time I found the correct place to hack in my changes. I then wrote a small HelloWord-like Python script which we could run through the hacked interpreter together.

I went to the developer with my Hello World and sat down next to him, and I asked him how many exceptions he expected to see. After convincing him that I was being honest and not trying to pull his leg he looked at the code and pointed at a single line. “There’s one”, he remarked, “and that’s probably it”. I then walked through the file with him and told him where I expected Exceptions to happen. My final count was 125 exceptions. “That’s more than I thought”, he chuckled.

From his laugh I realized he still wasn’t convinced. He probably thought that either I was exaggerating or that he could still manually step through 125 exceptions if he really needed to.

We started the hacked interpreter and looked at the output on the screen. Within a second the script was done running and we were looking at the final count… 2213 Exceptions.

“Oh”, he said, followed by a period of silence. “...ah”, he concluded.


For the nerds, the changes I made to CPython:

diff --git a/Python/errors.c b/Python/errors.c
index 61dc597916..0323263c37 100644
--- a/Python/errors.c
+++ b/Python/errors.c
@@ -29,6 +29,8 @@ _Py_IDENTIFIER(builtins);
 _Py_IDENTIFIER(stderr);
 _Py_IDENTIFIER(flush);

+static int errorcounter = 0;
+
 /* Forward declarations */
 static PyObject *
 _PyErr_FormatV(PyThreadState *tstate, PyObject *exception,
@@ -100,6 +102,16 @@ _PyErr_CreateException(PyObject *exception, PyObject *value)
 void
 _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
 {
+    PyObject* debug_repr;
+    PyObject* debug_str;
+    debug_repr = PyObject_Repr(value);
+    debug_str = PyUnicode_AsEncodedString(debug_repr, "utf-8", "~E~");
+    const char *debug_exception_text = PyBytes_AS_STRING(debug_str);
+    debug_repr = PyObject_Repr(exception);
+    debug_str = PyUnicode_AsEncodedString(debug_repr, "utf-8", "~E~");
+    const char *debug_exception_type = PyBytes_AS_STRING(debug_str);
+    fprintf(stderr, "DEBUG: %i _PyErr_SetObject: %s(%s)\n", ++errorcounter, debug_exception_type, debug_exception_text);
+
     PyObject *exc_value;
     PyObject *tb = NULL;

All the exceptions we saw passing by:

  • 1496x <class 'AttributeError'>
  • 233x <class 'KeyError'>
  • 212x <class 'IndexError'>
  • 88x <class 'TypeError'>
  • 85x <class 'StopIteration'>
  • 36x <class 'FileNotFoundError'>
  • 29x <class 'str'>
  • 22x <class 'GeneratorExit'>
  • 10x <class 'zipimport.ZipImportError'>
  • 10x <class 'ModuleNotFoundError'>
  • 5x <class 'NameError'>
  • 4x <class 'ValueError'>
  • 4x <class 'OSError'>
  • 3x <class 'ImportError'>
  • 3x <class 'bytes'>
  • 1x <class 'SystemExit'>
  • 1x <class 'SystemError'>
  • 1x <class 'OverflowError'>
  • 1x <class 'LookupError'>
  • 1x <class 'io.UnsupportedOperation'>