Raise if... "prod"❗¶
Last weekend I was meeting with a former colleague of mine and we were discussing about some python code, as one does during a weekend.
While going through some high level code and architecture, he explained to me how they are using a python decorator called non_prod_only
to raise an exception if a certain piece of code is run in production.
The idea is very simple, but effective at the same time. Whenever you decorate a function with @non_prod_only
, you prevent your future self (that will be pushing to main
) to remember that such functionality shouldn't end up in production.
And even if it does, you will get a nice exception that will save you from running the function.
The decorator¶
Naive implementation of such decorator would be something along the following lines:
import os
from functools import wraps
def is_prod():
return os.environ.get("ENV", "").lower() in {"prod", "production"} # (1)
def non_prod_only(func):
@wraps(func)
def wrapper(*args, **kwargs):
if is_prod():
raise Exception("This function is not allowed in production environment") # (2)
return func(*args, **kwargs)
return wrapper
- Replace with whatever equivalent way you have to detect prod!
- Please have some imagination and write a much more creative custom
Exception
!
The larger picture¶
Now, in the larger scheme of things, a decorator such as non_prod_only
is far away from the perfect solution to saving you from all the production troubles.
Yet, I still believe that there is a lot of value in abstracting its idea to a more general raise_if
decorator, aiming at customization and flexibility.
from functools import wraps
def raise_if(condition, exception = Exception, message = ""):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if condition():
raise exception(message)
return func(*args, **kwargs)
return wrapper
return decorator
Oh gosh! That's a lot of nested functions! Let's break it down:
raise_if
is a function that takes acondition
1 and returns a decorator- The decorator takes a function
func
and returns a wrapper - The wrapper checks the
condition
and:- raises an
exception
if it isTrue
- otherwise, it returns the result of
func
- raises an
If you want to read more about decorators with arguments, I wrote about it in the deczoo documentation.
Type Annotation¶
Since many people (me included) are big fans of type annotations, let's try to add them to our raise_if
decorator:
from functools import wraps
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def raise_if(
condition: Callable[[], bool],
exception: Exception = Exception,
message: str = ""
):
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
if condition():
raise exception(message)
return func(*args, **kwargs)
return wrapper
return decorator
-
In this implementation
condition
is a callable that takes no arguments and returns a boolean value. It is evaluated at runtime, so it can be as complex as you want it to be. ↩