Python is a dynamic language, and you can't know, up front, what exceptions a function could throw.
Take this example:
def throw(exception):
raise exception
What exception will that function raise? I can use throw(ValueError)
or throw(TypeError('foobar'))
, and both would work and are valid Python:
>>> throw(ValueError)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in throw
ValueError
>>> throw(TypeError('foobar'))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in throw
TypeError: foobar
Exceptions are just classes and instances. Current versions of Python require that the exception class must derive from BaseException
, but in old Python versions you could even use strings for exceptions (raise "Your mother was a hamster"
).
And because they are looked up as globals and are not reserved names, you can assign different exceptions to names. The following is legal Python syntax too:
>>> def oops():
... raise ValueError('Oops')
...
>>> ValueError = TypeError
>>> oops()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in oops
TypeError: Oops
That's why Python functions can't expose what exceptions they raise.
Note that there is never a good reason to use plain Exception
. Use one of the standard exceptions where they make sense (ValueError
, TypeError
, IndexError
, KeyError
, etc.) or create your own API-specific exceptions by subclassing from Exception
or a more specific exception subclass.
Then document your API properly. State what exceptions a developer should expect, where needed. The standard exceptions don't need to be spelled out; it is reasonably obvious that a function that only works on strings will throw TypeError
if you pass in a file object instead.
You can use a exception class hierarchy in your business application if you need to catch multiple types:
class BusinessException(Exception):
"""The base exception for all of Business APIs"""
class SpecificBusinessException(BusinessException):
"""Exception that indicates a specific problem occurred"""
class DifferenBusinessException(BusinessException):
"""Exception that indicates a different specific problem occurred"""
then raise the subclassed exceptions and catch BusinessException
to handle all, or catch only specific subclasses to customise handling.
If you must figure out what exceptions code raise and accept the risks involved with a dynamic language being able to change the names, then you could use abstract syntax tree (AST) analysis to at least find some information on exceptions. For straight raise Name
and raise Name(args..)
statements, extracting those names or calls by walking the AST is at least relatively straightforward:
import builtins
import inspect
import ast
class ExceptionExtractor(ast.NodeVisitor):
def __init__(self):
self.exceptions = []
def visit_Raise(self, node):
if node.exc is None:
# plain re-raise
return
exc_name = node.exc
if isinstance(exc_name, ast.Call):
exc_name = exc_name.func
if not (isinstance(exc_name, ast.Name) and
isinstance(exc_name.ctx, ast.Load)):
# not a raise Name or raise Name(...)
return
self.exceptions.append(exc_name.id)
def global_exceptions_raised(func):
"""Extract the expressions used in raise statements
Only supports raise Name and raise Name(...) forms, and
only if the source can be accessed. No checks are made for the
scope of the name.
returns references to those exception names that can be loaded from
the function globals.
"""
source = inspect.getsource(func)
tree = ast.parse(source)
extractor = ExceptionExtractor()
extractor.visit(tree)
fglobals = {**func.__globals__, **vars(builtins)}
return [fglobals[name] for name in extractor.exceptions if name in fglobals]