Whenever I need to apply some runtime constraints on a value while building an API, I usually compare the value to an expected range and raise a ValueError if it's not within the range. For example, let's define a function that throttles some fictitious operation. The throttle function limits the number of times an operation can be performed by specifying the throttle_after parameter. This parameter defines the number of iterations after which the operation will be halted. The current_iter parameter tracks the current number of times the operation has been performed. Here's the implementation:

# src.py
def throttle(current_iter: int, throttle_after: int = -1) -> None:
    """
    The value of 'throttle_after' must be -1 or an integer
    greater than 0. Here, -1 means no throttling, and 'n'
    means that the function will throttle some operation
    after 'n' iterations.

    The `current_iter` parameter denotes the current iteration
    of some operation. When 'current_iter > throttle_after' this
    function will throttle the operation.
    """

    # Return early if 'throttle_after=-1'.
    if throttle_after == -1:
        print("No throttling.")
        return

    # Ensure 'current_iter' is a positive integer.
    if not (isinstance(current_iter, int) and current_iter >= 0):
        raise ValueError(
            "Value of 'current_iter' must be a" " positive integer."
        )

    # Ensure 'throttle_after' is a non-zero positive integer.
    if not (isinstance(throttle_after, int) and throttle_after > 0):
        raise ValueError(
            "Value of 'throttle_after' must be either -1 or an"
            " integer greater than 0."
        )

    # Do the throttling.
    if current_iter > throttle_after:
        print(f"Thottling after {throttle_after} iteration(s).")
        return


if __name__ == "__main__":
    # Prints 'Throttling after 1 iteration(s).'
    throttle(current_iter=2, throttle_after=1)

We return early if the value of throttle_after is -1. Otherwise, we check to see if current_iter is a positive integer and throttle_after is a non-zero positive integer. If not, we raise a ValueError. When the parameters pass these checks then we compare current_iter with throttle_after. If the value of current_iter exceeds that of the throttle_after parameter, we throttle the operation.

While this works fine, recently, I've started to use assert to replace the conditionals with ValueError pattern. It works as follows:

# src.py
def throttle(current_iter: int, throttle_after: int = -1) -> None:
    # Return early if 'throttle_after=-1'.
    if throttle_after == -1:
        print("No throttling.")
        return

    # Ensure 'current_iter' is a positive integer.
    assert (
        isinstance(current_iter, int) and current_iter >= 0
    ), "Value of 'current_iter' must be a positive integer."

    # Ensure 'throttle_after' is a non-zero positive integer.
    assert isinstance(throttle_after, int) and throttle_after > 0, (
        "Value of 'throttle_after' must be either -1 or an "
        " integer greater than 0."
    )

    # Do the throttling.
    if current_iter > throttle_after:
        print(f"Thottling after {throttle_after} iterations.")
        return


if __name__ == "__main__":
    # AssertionError: Value of 'current_iter' must be a positive
    # integer.
    throttle(current_iter=-2, throttle_after=1)

So, instead of using the if not expression ... raise ValueError pattern, we can leverage assert expression, "Error message" pattern. In the latter case, assert will raise AssertionError with the "Error message" if the expression evaluates to a falsy value. Otherwise, the statement will remain silent and allow the execution to move forward.

This is more succinct and makes the code flatter. I've no idea why I haven't started using it earlier and this piece of code in the Starlette repository jolted my brain. Eh bien, better late than never, I guess.

After this blog was published, several people mentioned on Twitter that the second approach has a small caveat. Python has a flag that allows you to disable assert statements in a script. You can disable the assertions in the snippet above by running the script with the -OO flag:

python -00 src.py

Removing assert statements will disable the constraints needed for the second throttle function to work, which could lead to unexpected behavior or even subtle bugs. However, I see this being used frequently in frameworks like Starlette and FastAPI. Also, from my experience, using assertions is much more common than running production code with the optimization flag.


Published

Category

python

Tags

Stay in Touch