How to list all exceptions a function could raise in Python 3?

You can’t get reliable results for some (if not most) functions. Some examples:

  • functions that execute arbitrary code (e.g. exec(')(rorrEeulaV esiar'[::-1]) raises ValueError)

  • functions that aren’t written in Python

  • functions that call other functions that can propagate errors to the caller

  • functions re-raising active exceptions in the except: block

Unfortunately, this list is incomplete.

E.g. os.makedirs is written in Python and you can see its source:

...
try:
    mkdir(name, mode)
except OSError as e:
    if not exist_ok or e.errno != errno.EEXIST or not path.isdir(name):
        raise

Bare raise re-raises the last active exception (OSError or one of its subclasses). Here’s the class hierarchy for OSError:

+-- OSError
|    +-- BlockingIOError
|    +-- ChildProcessError
|    +-- ConnectionError
|    |    +-- BrokenPipeError
|    |    +-- ConnectionAbortedError
|    |    +-- ConnectionRefusedError
|    |    +-- ConnectionResetError
|    +-- FileExistsError
|    +-- FileNotFoundError
|    +-- InterruptedError
|    +-- IsADirectoryError
|    +-- NotADirectoryError
|    +-- PermissionError
|    +-- ProcessLookupError
|    +-- TimeoutError

To get the exact exception types you’ll need to look into mkdir, functions it calls, functions those functions call etc.

So, getting possible exceptions without running the function is very hard and you really should not do it.


However for simple cases like

raise Exception # without arguments
raise Exception('abc') # with arguments

a combination of ast module functionality and inspect.getclosurevars (to get exception classes, was introduced in Python 3.3) can produce quite accurate results:

from inspect import getclosurevars, getsource
from collections import ChainMap
from textwrap import dedent
import ast, os

class MyException(Exception):
    pass

def g():
    raise Exception

class A():
    def method():
        raise OSError

def f(x):
    int()
    A.method()
    os.makedirs()
    g()
    raise MyException
    raise ValueError('argument')


def get_exceptions(func, ids=set()):
    try:
        vars = ChainMap(*getclosurevars(func)[:3])
        source = dedent(getsource(func))
    except TypeError:
        return

    class _visitor(ast.NodeTransformer):
        def __init__(self):
            self.nodes = []
            self.other = []

        def visit_Raise(self, n):
            self.nodes.append(n.exc)

        def visit_Expr(self, n):
            if not isinstance(n.value, ast.Call):
                return
            c, ob = n.value.func, None
            if isinstance(c, ast.Attribute):
                parts = []
                while getattr(c, 'value', None):
                    parts.append(c.attr)
                    c = c.value
                if c.id in vars:
                    ob = vars[c.id]
                    for name in reversed(parts):
                        ob = getattr(ob, name)

            elif isinstance(c, ast.Name):
                if c.id in vars:
                    ob = vars[c.id]

            if ob is not None and id(ob) not in ids:
                self.other.append(ob)
                ids.add(id(ob))

    v = _visitor()
    v.visit(ast.parse(source))
    for n in v.nodes:
        if isinstance(n, (ast.Call, ast.Name)):
            name = n.id if isinstance(n, ast.Name) else n.func.id
            if name in vars:
                yield vars[name]

    for o in v.other:
        yield from get_exceptions(o)


for e in get_exceptions(f):
    print(e)

prints

<class '__main__.MyException'>
<class 'ValueError'>
<class 'OSError'>
<class 'Exception'>

Keep in mind that this code only works for functions written in Python.

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)