Python: Multiprocessing and Exceptions

Python’s multiprocessing module provides an interface for spawning and managing child processes that is familiar to users of the threading module. One problem with the multiprocessing module, however, is that exceptions in spawned child processes don’t print stack traces:

Consider the following snippet:

import multiprocessing
import somelib

def f(x):
  return 1 / somelib.somefunc(x)

if __name__ == '__main__':
  with multiprocessing.Pool(5) as pool:
    print(pool.map(f, range(5)))

and the following error message:

Traceback (most recent call last):
  File "test.py", line 9, in <module>
    print(pool.map(f, range(5)))
  File "/usr/lib/python3.3/multiprocessing/pool.py", line 228, in map
    return self._map_async(func, iterable, mapstar, chunksize).get()
  File "/usr/lib/python3.3/multiprocessing/pool.py", line 564, in get
    raise self._value
ZeroDivisionError: division by zero

What triggered the ZeroDivisionError? Did somelib.somefunc(x) return 0, or did some other computation in somelib.somefunc() cause the exception? You will notice that we only see the stack trace of the main process, whereas the stack trace of the code that actually triggered the exception in the worker processes is not shown at all.

Luckily, Python provides a handy traceback module for working with exceptions and stack traces. All we have to do is catch the exception inside the worker process, and print it. Let’s change the code above to read:

import multiprocessing
import traceback
import somelib

def f(x):
  try:
    return 1 / somelib.somefunc(x)
  except Exception as e:
    print('Caught exception in worker thread (x = %d):' % x)

    # This prints the type, value, and stack trace of the
    # current exception being handled.
    traceback.print_exc()

    print()
    raise e

if __name__ == '__main__':
  with multiprocessing.Pool(5) as pool:
    print(pool.map(f, range(5)))

Now, if you run the same code again, you will see something like this:

Caught exception in worker thread (x = 0):
Traceback (most recent call last):
  File "test.py", line 7, in f
    return 1 / somelib.somefunc(x)
  File "/path/to/somelib.py", line 2, in somefunc
    return 1 / x
ZeroDivisionError: division by zero

Traceback (most recent call last):
  File "test.py", line 16, in <module>
    print(pool.map(f, range(5)))
  File "/usr/lib/python3.3/multiprocessing/pool.py", line 228, in map
    return self._map_async(func, iterable, mapstar, chunksize).get()
  File "/usr/lib/python3.3/multiprocessing/pool.py", line 564, in get
    raise self._value
ZeroDivisionError: division by zero

The printed traceback reveals somelib.somefunc() to be the actual culprit.

In practice, you may want to save the exception and the stack trace somewhere. For that, you can use the file argument of print_exc in combination with StringIO. For example:

import logging
import io  # Import StringIO in Python 2
...

def Work(...):
  try:
    ...
  except Exception as e:
    exc_buffer = io.StringIO()
    traceback.print_exc(file=exc_buffer)
    logging.error(
        'Uncaught exception in worker process:\n%s',
        exc_buffer.getvalue())
    raise e

About the author

I am a software engineer by profession and a passionate technology geek in my free time.