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 here and here
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:
Index | Function | Input | Performance |
---|---|---|---|
0 | clamp_float_with_branching_flat | Lower Bound | 0.070996 |
1 | clamp_float_with_branching_nested | Lower Bound | 0.070725 |
2 | clamp_float_with_min_and_max | Lower Bound | 0.144475 |
3 | clamp_float_with_sorting | Lower Bound | 0.1521 |
4 | control | Lower Bound | 0.068862 |
5 | clamp_float_with_branching_flat | Upper Bound | 0.075774 |
6 | clamp_float_with_branching_nested | Upper Bound | 0.07743 |
7 | clamp_float_with_min_and_max | Upper Bound | 0.141509 |
8 | clamp_float_with_sorting | Upper Bound | 0.168366 |
9 | control | Upper Bound | 0.068561 |
10 | clamp_float_with_branching_flat | Between Bounds | 0.077869 |
11 | clamp_float_with_branching_nested | Between Bounds | 0.07806 |
12 | clamp_float_with_min_and_max | Between Bounds | 0.144185 |
13 | clamp_float_with_sorting | Between Bounds | 0.161859 |
14 | control | Between Bounds | 0.069145 |
15 | clamp_float_with_branching_flat | Large Numbers | 0.070954 |
16 | clamp_float_with_branching_nested | Large Numbers | 0.072033 |
17 | clamp_float_with_min_and_max | Large Numbers | 0.134869 |
18 | clamp_float_with_sorting | Large Numbers | 0.145615 |
And if you prefer, here’s a quick visualization:

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 #RenegadePython. If I see it, I’ll give it a share! If you have questions about this challenge, feel free to hit me up on Discord
.
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:
- How to Convert sqlite3 Rows into Python Objects
- 5 Ways to Write Hello World in Python
- How to Use Python to Build a Simple Visualization Dashboard Using Plotly
Also, if you’re interested, here are some additional Python resources (#ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
Thanks again for hanging out! Until next time.
Recent Posts
As of March of this year, I'm off Twitter for good! In fact, at the time of writing, my old account should be deleted. Let's talk about that!
Recently, I was thinking about how there are so many ways to approach software design. While some of these approaches have fancy names, I'm not sure if anyone has really thought about them...