Python Functional Programming – Exception Handling Techniques

error handlingfunctional programmingpython

I've been told that in functional programming one is not supposed to throw and/or observe exceptions. Instead an erroneous calculation should be evaluated as a bottom value. In Python (or other languages that do not fully encourage functional programming) one can return None(or another alternative treated as the bottom value, though None doesn't strictly comply with the definition) whenever something goes wrong to "remain pure", but to do so one has to observe an error in the first place, i.e.

def fn(*args):
    try:
        ... do something
    except SomeException:
        return None

Does this violate purity? And if so, does it mean, that it is impossible to handle errors purely in Python?

Update

In his comment Eric Lippert reminded me of another way to treat exceptions in FP. Though I've never seen that done in Python in practice, I played with it back when I studied FP a year ago. Here any optional-decorated function returnsOptional values, which can be empty, for normal outputs as well as for a specified list of exceptions (unspecified exceptions still can terminate the execution). Carry creates a delayed evaluation, where each step (delayed function call) either gets a nonempty Optional output from the previous step and simply passes it on, or otherwise evaluates itself passing a new Optional. In the end the final value is either normal or Empty. Here the try/except block is hidden behind a decorator, so the specified exceptions can be regarded as part of the return type signature.

class Empty:
    def __repr__(self):
        return "Empty"


class Optional:
    def __init__(self, value=Empty):
        self._value = value

    @property
    def value(self):
        return Empty if self.isempty else self._value

    @property
    def isempty(self):
        return isinstance(self._value, BaseException) or self._value is Empty

    def __bool__(self):
        raise TypeError("Optional has no boolean value")


def optional(*exception_types):
    def build_wrapper(func):
        def wrapper(*args, **kwargs):
            try:
                return Optional(func(*args, **kwargs))
            except exception_types as e:
                return Optional(e)
        wrapper.__isoptional__ = True
        return wrapper
    return build_wrapper


class Carry:
    """
    >>> from functools import partial
    >>> @optional(ArithmeticError)
    ... def rdiv(a, b):
    ...     return b // a
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 0) >> partial(rdiv, 1))(1)
    1
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 1))(1)
    1
    >>> (Carry() >> rdiv >> rdiv)(0, 1) is Empty
    True
    """
    def __init__(self, steps=None):
        self._steps = tuple(steps) if steps is not None else ()

    def _add_step(self, step):
        fn, *step_args = step if isinstance(step, Sequence) else (step, )
        return type(self)(steps=self._steps + ((fn, step_args), ))

    def __rshift__(self, step) -> "Carry":
        return self._add_step(step)

    def _evaluate(self, *args) -> Optional:
        def caller(carried: Optional, step):
            fn, step_args = step
            return fn(*(*step_args, *args)) if carried.isempty else carried
        return reduce(caller, self._steps, Optional())

    def __call__(self, *args):
        return self._evaluate(*args).value

Best Answer

First of all, let's clear up some misconceptions. There is no "bottom value". The bottom type is defined as a type that is a subtype of every other type in the language. From this, one can prove (in any interesting type system at least), that the bottom type has no values - it is empty. So there is no such thing as a bottom value.

Why is the bottom type useful? Well, knowing that it's empty let's us make some deductions on program behavior. For example, if we have the function:

def do_thing(a: int) -> Bottom: ...

we know that do_thing can never return, since it would have to return a value of type Bottom. Thus, there are only two possibilities:

  1. do_thing does not halt
  2. do_thing throws an exception (in languages with an exception mechanism)

Note that I created a type Bottom which does not actually exist in the Python language. None is a misnomer; it is actually the unit value, the only value of the unit type, which is called NoneType in Python (do type(None) to confirm for yourself).

Now, another misconception is that functional languages do not have exception. This isn't true either. SML for example has a very nice exception mechanism. However, exceptions are used much more sparingly in SML than in e.g. Python. As you've said, the common way to indicate some kind of failure in functional languages is by returning an Option type. For example, we would create a safe division function as follows:

def safe_div(num: int, den: int) -> Option[int]:
  return Some(num/den) if den != 0 else None

Unfortunately, since Python doesn't actually have sum types, this isn't a viable approach. You could return None as a poor-man's option type to signify failure, but this is really no better than returning Null. There is no type-safety.

So I would advise following the language's conventions in this case. Python uses exceptions idiomatically to handle control flow (which is bad design, IMO, but it's standard nonetheless), so unless you're only working with code you wrote yourself, I'd recommend following standard practice. Whether this is "pure" or not is irrelevant.

Related Topic