Introduction
I recently changed someone else's Python code in a way that I thought fixed a problem. However, after showing them the change, they provided context that moved me to revert the commit and find a different way to handle the potential Exception
.
In my quest to elegantly handle the error in a functional and non-evasive way, I started on a path that took me to a very pleasing software development destination!
What am I talking about? I discovered (while perusing SO) the partial
function in the functools
module of Python's built-in library. And with this function, I created a re-usable error handling decorator that fits my use case nicely.
In this article, I'll share why using the partial
function to help me build a decorator was precisely the tool I needed.
Functional Programming with Typescript
In the last year and a half, I've become enamored with functional programming-inspired libraries in Typescript (TS):
- fp-ts
- monocle-ts
- io-ts
- lodash (honorable mention)
Understanding functional programming terms like closure, higher-order function, and partially applied function is important to using the libraries effectively. Consequently, I became accustomed to these tools and started reaching for them in my work with Python.
Back to Python
In the scenario presented earlier, I modified a Python function to handle a ZeroDivisonError
. However, later, I was told the ZeroDivisonError
only happens when the user makes a mistake. Therefore, instead of handling this error and hiding the user's mistake, we should catch it and alert the user that they may be doing something undesirable.
Now that I knew I needed to catch the error, and not prevent it, I no longer needed to change the function that someone else had written. Instead, I wanted to wrap it in a try
and except
block so I could present a custom error message to the user.
Higher Order Functions
I started searching for how higher-order functions work in Python and came across an article on freeCodeCamp by Roy Chng titled Python Decorators Explained For Beginners.
"Decorators give you the ability to modify the behavior of functions without altering their source code, providing a concise and flexible way to enhance and extend their functionality."
Roy Chng, Python Decorators Explains for Beginners
Perfect, wrapping a function is something I'm very comfortable with from my knowledge of TS and higher-order functions. However, I still wanted to check on how Python defines decorators:
"A function returning another function usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are classmethod() and staticmethod(). [...] The decorator syntax is merely syntactic sugar [...]"
Python Docs, decorator
The examples given in Python's documentation are instructive: classmethod() and staticmethod().
# https://docs.python.org/3/glossary.html#term-decorator
def f(arg):
...
f = staticmethod(f)
@staticmethod
def f(arg):
...
I want to pass the other programmer's function to my function and execute it inside a try and except block. The decorator function and usage may look something like the code below:
def zero_division_error(calc_func):
def inner(*args, **kwargs):
try:
return calc_func(*args, **kwargs)
except ZeroDivisionError as e:
# Possibly log error
raise ValueError('Your other inputs were invalid') from e
return inner
@zero_division_error
def calc(x, y):
return x / y
print(calc(10, 0))
# ZeroDivisionError: division by zero
# ValueError: Your other inputs were invalid
This code is certainly good enough. However, I want more. Instead of returning the default ZeroDivisionError
message, I want the function to return a custom message passed in by the developer. Consequently, I'll need to find a way of passing the error message argument to the decorator.
I know that passing arguments to decorators can be done because many libraries have decorators that do this (thinking of FastAPI).
# FastAPI example API route
@router.put(
"",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(get_current_active_user)],
)
def create(value: Value):
# handle PUT request
First things first, let's discuss why this is useful and how closures are helpful.
Closures
"A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time."
MDN WebDocs, Closure
Previously, while using TS, closures have been a way to save a variable until later use, a way to "preload" a reusable function with some targeted information. Initially, I wasn't sure I could use closures in Python because I had not seen the syntax (despite having used closures unknowingly with FastAPI).
In TS, writing a closure may look like:
const multiply = (x: number) => (y: number) => y * x
const hoursToMinutes = multiply(60)
const minutesToSeconds = multiply(60)
// hoursToMinutes(5)
// 300
But you can't do this (in the same way) in Python. Still, adding the lexical (closure) scope to a function can be very useful in building flexible software functions.
So, how can I write this function to only take a function as its last argument but also take other arguments before that?
Partial
In my search, I found this SO answer that solved my problems. The answer gives the example:
# srj
# https://stackoverflow.com/questions/5929107/decorators-with-parameters/25827070#25827070
from functools import partial
def _pseudo_decor(fun, argument):
def ret_fun(*args, **kwargs):
#do stuff here, for eg.
print ("decorator arg is %s" % str(argument))
return fun(*args, **kwargs)
return ret_fun
real_decorator = partial(_pseudo_decor, argument=arg)
@real_decorator
def foo(*args, **kwargs):
pass
Above, the example introduces the partial
function that is part of the functools
module in Python. With the partial
function, I can provide my custom error message before passing in any arbitrary function (hopefully a mathematical one).
Then, the decorator function can access the error message in the closure (lexical) scope.
from typing import Union
from functools import partial
def custom_zero_division_error(calc_func, errorMessage: Union[None, str] = None):
def inner(*args, **kwargs):
try:
return calc_func(*args, **kwargs)
except ZeroDivisionError as e:
if errorMessage is not None:
# Provide custom message, keeping stack trace from ZeroDivisionError
raise ValueError(errorMessage) from e
else:
# Re-raise error but keep stack trace
raise
return inner
real_decorator = partial(custom_zero_division_error, errorMessage='One of your inputs is a valid type, but is a duplicate of the previous value, which may be zero.')
@real_decorator
def calc(x, y):
return x / y
print(calc(10, 0))
# ZeroDivisionError: division by zero
# ValueError: One of your inputs is a valid type, but is a duplicate of the previous value, which may be zero.
If you start chaining higher-order functions together you may have a situation where a function returns two functions before returning a value. Referring back to the TS example, multiply(60)
is a partially applied function because it's still waiting for the second number to multiply by. That's where the partial
function in Python draws its name.
More Decorator Examples with (better) Explanations
The SO answer links to a PyCon talk where the presenter explains decorators nicely. He also works through five examples of decorator usage in Python.
It's a helpful video and worth the watch. I was excited to see one of the examples use memoization, which I had just written about in my last article, albeit in TS.
Conclusion
Programming paradigms in one language show up in other languages. Learning about functional programming techniques in TS helped me navigate an often confusing topic for Python developers.
It reminds me of an advertisement I saw at a gas station on the digital display once, however, I'm not sure if it's a perfect fit. Anyway, it read, "All roads lead to more roads, and that's pretty cool."