How to Clamp a Floating Point Number in Python: Branching, Sorting, and More!

How to Clamp a Floating Point Number in Python Featured Image

If you’ve ever tried to force a number between some bounds, you know how much of a nightmare it can be. Just one missing line of code can cause your value to overflow or underflow the bounds, so what do you do? Turns out there’s a term for this problem, clamping, and you can use it to solve all of your bounding issues.

As it turns out, there are a lot of ways to clamp a floating point number in Python. In terms of speed, a ternary solution such as minimum if num < minimum else maximum if num > maximum else num seems to be the fastest. Alternatively, there are clever solutions that take advantage of tricks like sorting, such as sorted([num, minimum, maximum])[1], and other solutions that use the min and max functions, max(min(num, maximum), minimum). All solutions are great ways to force a number between two bounds.

With all that out of the way, let’s get into the details.

Table of Contents

Problem Description

Recently, I was trying to build out a Discord bot with videogame-like features. For instance, I wanted my bot to have features like health and damage. To do that, I needed some sort of global fields that I could update as events occur. Eventually, I settled on floating point values because I wanted numbers that I could infinitely divide (in theory).

Unfortunately, one of the challenges with tracking floating point values is that they aren’t easy to keep within certain bounds. For instance, if you want to track HP, you probably have some maximum value and some minimum value (usually 0). If an event triggers damage greater than the bots current health, we probably don’t want the health to go to zero. So what do we do?

Well, as it turns out, this problem is well documented in the space, enough to have its own term: clamping. Today, we’ll talk about how to clamp values between some bounds.

Solutions

Once again, I like to dive into the homemade solutions first, before we get into anything useful. If you prefer to get straight to the better solutions, feel free to skip ahead. Otherwise, let’s get into it!

Clamp a Float by Anticipating Possible Result

When I first encountered this problem, I had a number that I wanted to bound between 0 and .40. To do that, I was extremely careful to make sure any changes in the value were within that range. For example, I chose the value .002 to move around in that range. That way, I would only have to check if a change in the value would breach the range:

num = 0
if num - .002 >= 0:
    num = num - .002

As you may know, however, this particular condition is only possible because of the increment we picked. Specifically, assuming .002 is the only value we add and subtract, then we’re guaranteed to hit the bounds exactly (barring any nasty floating point rounding).

If instead we had some value that did not divide evenly into the range, we could end up with scenarios where the change doesn’t occur when it otherwise should force the value to one of the extremes (e.g., .01 – .015 should force to zero). Likewise, there are issues of floating point rounding where the equality check is useless. Luckily, there are better solutions.

Clamp a Float By Fixing Result

A decent way to fix the issues from the previous problem is to setup some branches that catch overflow. For instance, in the previous example, it doesn’t matter what value is being subtracted since you can force the result to zero:

num -= subtractor
if num < 0:
    num = 0

It’s subtle, but the idea is that we no longer have to check if the subtraction (or addition) is legal. We can perform the operation then check if it’s out of bounds. If it is, we just force the result to the appropriate bound.

Unfortunately, the downside to a solution like this is that we have to write a condition like this every time we need to modify a value. What if there was a function we could use to handle the clamping for us?

Clamp a Float With Branching

By far, my personal favorite way to handle clamping is to write a function that always returns the appropriate value. Here’s how you might do that:

def clamp(num, minimum, maximum) -> float:
    if num < minimum:
        return minimum
    elif num > maximum:
        return maximum
    else:
        return num

Then, you can plug this function in anywhere you need it:

num = clamp(num - .002, 0, .4)

And if you like fancy one-liners, here’s the same solution using ternary operators:

def clamp(num, minimum, maximum) -> float:
    return minimum if num < minimum else maximum if num > maximum else num

But wait, there’s more!

Clamp a Float With Min and Max

While the previous solution is perfectly, the one I tend to see more often makes use of the min and max functions in Python, and it looks like this:

def clamp(num, minimum, maximum) -> float:
    return max(min(num, maximum), minimum)

The idea here is that we compute the smaller of our value and the maximum. Then, we compute the bigger of the minimum and the result we just computed. The result is a number that is always in the appropriate range. Would you believe there are even more options?

Clamp a Float With Sorting

One clever way to clamp a float is to take the three values we’ve been using and sort them. As a result, the middle value would always be the value we want:

def clamp(num, minimum, maximum) -> float:
    return sorted([num, minimum, maximum])[1]

We’ll talk about how well this performs later, but the cleverness of this function makes me like it quite a bit. In fact, it makes me rethink what clamping really is (i.e., finding some middle value between extremes). To me, that’s a pretty cool way of conceptualizing it.

With that said, if you’re sad that there is not built-in way to solve this problem, you’re not alone. To my knowledge, there is no clamping function in Python (see hereOpens in a new tab. and hereOpens in a new tab. for discussion on why one doesn’t exist). The closest you’ll get is the clip function from numpy and the clamp function from PyTorch, but it’s always silly to import a massive library for a single function. So, it looks like you’re stuck with writing it yourself.

Performance

It’s been ages since I’ve written on of these how to Python articles, so I’ve honestly forgotten how to performance test code. Luckily, I made myself a test bench that I continue to modify and update. Here’s a copy of the test suite:

def control(*_) -> None:
    """
    Provides a control scenario for testing. In this case, none of the functions
    share any overhead, so this function is empty.

    :param _: a placeholder for the string input
    :return: None
    """
    pass


def clamp_float_with_branching_nested(num: float, minimum: float, maximum: float) -> float:
    """
    Clamps a float between two bounds using a series of if statements.

    :param num: the value to clamp
    :param minimum: the lower bound
    :param maximum: the upper bound
    :return: a value in the range of minimum and maximum
    """
    if num < minimum:
        return minimum
    elif num > maximum:
        return maximum
    else:
        return num


def clamp_float_with_branching_flat(num: float, minimum: float, maximum: float) -> float:
    """
    Clamps a float between two bounds using a series of ternary statements.

    :param num: the value to clamp
    :param minimum: the lower bound
    :param maximum: the upper bound
    :return: a value in the range of minimum and maximum
    """
    return minimum if num < minimum else maximum if num > maximum else num


def clamp_float_with_min_and_max(num: float, minimum: float, maximum: float) -> float:
    """
    Clamps a float between two bounds using a mix of min and max functions.

    :param num: the value to clamp
    :param minimum: the lower bound
    :param maximum: the upper bound
    :return: a value in the range of minimum and maximum
    """
    return max(min(num, maximum), minimum)


def clamp_float_with_sorting(num: float, minimum: float, maximum: float) -> float:
    """
    Clamps a float between two bounds using a sorting technique.

    :param num: the value to clamp
    :param minimum: the lower bound
    :param maximum: the upper bound
    :return: a value in the range of minimum and maximum
    """
    sorted([num, minimum, maximum])[1]


if __name__ == "__main__":
    test_bench(
        {
            "Lower Bound": [-.002, 0, .40],
            "Upper Bound": [.402, 0, .40],
            "Between Bounds": [.14, 0, .4],
            "Large Numbers": [123456789, -432512317, 5487131463]
        }
    )

As a heads up, note that some of the solutions are missing because I couldn’t come up with a meaningful way to test them. At any rate, here are the results:

IndexFunctionInputPerformance
0clamp_float_with_branching_flatLower Bound0.070996
1clamp_float_with_branching_nestedLower Bound0.070725
2clamp_float_with_min_and_maxLower Bound0.144475
3clamp_float_with_sortingLower Bound0.1521
4controlLower Bound0.068862
5clamp_float_with_branching_flatUpper Bound0.075774
6clamp_float_with_branching_nestedUpper Bound0.07743
7clamp_float_with_min_and_maxUpper Bound0.141509
8clamp_float_with_sortingUpper Bound0.168366
9controlUpper Bound0.068561
10clamp_float_with_branching_flatBetween Bounds0.077869
11clamp_float_with_branching_nestedBetween Bounds0.07806
12clamp_float_with_min_and_maxBetween Bounds0.144185
13clamp_float_with_sortingBetween Bounds0.161859
14controlBetween Bounds0.069145
15clamp_float_with_branching_flatLarge Numbers0.070954
16clamp_float_with_branching_nestedLarge Numbers0.072033
17clamp_float_with_min_and_maxLarge Numbers0.134869
18clamp_float_with_sortingLarge Numbers0.145615

And if you prefer, here’s a quick visualization:

A visual bar graph comparison of performances for four clamping solutions beside a control.

As you can see, the branching options are a bit faster than anything else—only slightly slower than doing nothing. Therefore, if I had to pick a solution based on these metrics, I’d grab the nested branching solution. As always, feel free to pick the solution that works best for you.

Challenge

As usual, let’s take a look at another challenge. Given that we’ve solved the issue of clamping a single value, what if we wanted to clamp values but also allow for wraparound? For instance, integers typically have some sort of limit (not in Python, of course). When that limit is hit, we get integer overflow causing the integer to wraparound to its minimum value. How would we accomplish a similar behavior in Python?

Still confused? Here are some sample inputs for our mystery function:

# function header: wraparound(value, minimum, maximum)
wraparound(5, 0, 2) # returns 1
wraparound(3, -1, 1) # returns 0

As always, if you have a solution, feel free to share it on Twitter (while it’s still alive) using #RenegadePythonOpens in a new tab.. If I see it, I’ll give it a share! If you have questions about this challenge, feel free to hit me up on DiscordOpens in a new tab..

A Little Recap

Alright, that’s all I wanted to cover to day. As usual, here are the solutions all in one place:

num = -.002
minimum = 0
maximum = 0.4

# clamp by if statements
if num < minimum:
    minimum
elif num > maximum:
    maximum
else:
    num

# clamp by flat ternary
minimum if num < minimum else maximum if num > maximum else num

# clamp with min/max
max(min(num, maximum), minimum)

# clamp by sorting
sorted([num, minimum, maximum])[1]

Once again, if you like this sort of thing and want to see more like it, check out my list of ways to grow the site. Otherwise, here are some related articles:

Also, if you’re interested, here are some additional Python resources (#ad):

Thanks again for hanging out! Until next time.

How to Python (42 Articles)—Series Navigation

The How to Python tutorial series strays from the usual in-depth coding articles by exploring byte-sized problems in Python. In this series, students will dive into unique topics such as How to Invert a Dictionary, How to Sum Elements of Two Lists, and How to Check if a File Exists.

Each problem is explored from the naive approach to the ideal solution. Occasionally, there’ll be some just-for-fun solutions too. At the end of every article, you’ll find a recap full of code snippets for your own use. Don’t be afraid to take what you need!

If you’re not sure where to start, I recommend checking out our list of Python Code Snippets for Everyday Problems. In addition, you can find some of the snippets in a Jupyter notebook format on GitHubOpens in a new tab.,

If you have a problem of your own, feel free to ask. Someone else probably has the same problem. Enjoy How to Python!

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife, playing Overwatch and Phantasy Star Online 2, practicing trombone, watching Penguins hockey, and traveling the world.

Recent Posts