A clamp function inside math to put an interval

I agree this.
so why not just put it in module math?
The implementation can be extremely easy:

# module: math
def clamp[T: int | float](value: T, min_val: T, max_val: T) -> T:
    if min_val > max_val:
        raise ValueError("min_val cannot be greater than max_val")
    return min(max(value, min_val), max_val)

I know sort()'s tricks can solve this,
and directly write min(max(value, min_val), max_val) can also solve this,
but why not add this? It’s faster, and it’s easier to read.

+1

the fact that it is “trivial to write” still implies there is a mental burden to write it whenever one needs it, instead of it just being there - this is a staple in other languages nowadays - and it used to be a differential to Python to have more of these trivial functionality so that one doesn’t need to take their mind off whatever the real problem they have at hand, to have to worry about writting a three line `clamp`, and them worry about it not being efficient enough, etc.

To illustrat the point that Python historically had more of such trivial functionality, we can just look at the `random` functions: it is trivial to write “random.choice”, random.randrange/randint - but Python had those when all the other languages had was a float from 0 to 1: it ever felt awkward to me, after coding in Python, to have to go back to “take the random 0-1 number, multiply, round, think if there are edge cases left-out”.

If this was for an easy thing to do, but too niche, and that no other language had, it would make sense to take some rounds of debate on whether it should be included. As it is, though, it should be a matter: “oops, we were missing that, thanks” (and them add it to the codebase)

The math module is not meant as a place where we dump convenient math functions. It’s the place meant to mimic math.h and expose such functions most of the time. We do have some exceptions and now we also have math.integer for arithmetic functions.

That being said, I still don’t think it’s a good idea to actually have it in the stdlib. I very much prefer having a dedicated clip/clamp depending on the inputs I use. If I work with numpy arrays, then I would use np.clip. The same reason why I would use np.minimum instead of min.

Note that the math module is also entirely written in C; there is no pure Python part so we need to deal with that at the C level; it’s not really an issue as we can do min/max ourselves, but still it’s annoying). Finally, min/max also support additional comparators, so we would likely need to also pass key to it.


I would very much appreciate if we had a place where to dump recipes or standard functions, but AFAIK, this is not the essence of the stdlib and I also think such recipes should be left to 3rd party dependencies. There is nothing wrong with having deps. Other languages may want to include it in their stdlib because it’s bothersome to rely on 3rd party libs or if the language’s purpose is directly related (I do expect having a clip() function for MatLab for instance; the language is meant for doing maths in the first place; I’m a bit surprised that R doesn’t though). Hence, I wouldn’t consider the list of given examples as a strong argument in favor of the addition.

1 Like

Various standard library modules (itertools, decimal, random to name a few) have a Recipes section, but math isn’t among them. Would this function and a few others (e.g. round-up division, although that may be better situated at math.integer) be a good starting point for such a section of the math documentation?
And sometimes -perhaps once a year?- a recipe ‘promotes’ to be a proper function in the library. So if clip or clamp is overwhelmingly popular that may happen at a later stage.

I think it would be reasonable to put some recipes somewhere, but we also try to avoid putting “common and simple” recipes in the docs as they tend to grow. itertools (or more generally, all your cited examples) is a good example of 3-lines nontrivial recipes but for math I would prefer not to have a section just for a single recipe, especially for a “simple” recipe that doesn’t need advanced concepts (in contrast to recipes in itertools where you may need some time to convince yourself of their correctness). For “round-up” division it may be worthwhile but this should be a separate topic/question (maybe there are more mathematical recipes that users need?)

We also usually don’t promote recipes to functions. It adds a maintenance burden among other problems (syncing the docs, rejecting proposals to enhance it or extend it, etc; once something is in the stdlib it’s very hard to remove it but it’s also hard to change it without a long deprecation period).

I’m not ashamed to say that I’ve always felt the same :sweat_smile:

“minimum of x and maximum value” and “maximum of x and minimum value” is very chiastic

How does this thread’s proposal address one-sided clamping though? Do we always want a clamp function that is two-sided, that is with both a minimum and maximum at the same time?

> developing a video game
> drinking a potion boosts health capacity to 150% for the rest of a level
> finish level with health at 120%
> starting the next level should reset us to health capacity of 100%
> we should clip off the top of the health bar value to leave us with health at most 100%
> okay do we want the maximum(?) of health and 100%
> err no that would leave us at 120%
> so we want the maximum(?) of health and 0%
> nope, that still leaves us with 120%
> ah we want the minimum of health and 100% right? *pauses to think*
> yeah that does it
> also if we finished the level at less than 100% health, say 80%, then min(health, 100%) leaves us with 80%, that’s fine

Simple one sided:

import functools

at_most_100_percent = functools.partial(min, 1.0)

print(at_most_100_percent(-0.5)) # -0.5
print(at_most_100_percent( 0.8)) #  0.8
print(at_most_100_percent( 0.9)) #  0.9
print(at_most_100_percent( 1.0)) #  1.0
print(at_most_100_percent( 1.1)) #  1.0
print(at_most_100_percent( 1.2)) #  1.0

becomes

import functools

def clamp(x, minimum=None, maximum=None):
    if minimum is not None and maximum is not None and minimum > maximum:
        raise ValueError
    ret = x
    if minimum is not None:
        ret = max(ret, minimum)
    if maximum is not None:
        ret = min(ret, maximum)
    return ret

at_most_100_percent = functools.partial(clamp, maximum=1.0)

print(at_most_100_percent(-0.5)) # -0.5
print(at_most_100_percent( 0.8)) #  0.8
print(at_most_100_percent( 0.9)) #  0.9
print(at_most_100_percent( 1.0)) #  1.0
print(at_most_100_percent( 1.1)) #  1.0
print(at_most_100_percent( 1.2)) #  1.0

when using a two-sided clamp function. I agree that functools.partial(clamp, maximum=1.0) is clearer than functools.partial(min, 1.0), especially when one is tired, but the implementation is checking the if minimum is not None etc conditions every single call of at_most_100_percent.

Maybe a factory approach is slightly more optimized but it looks a bit icky:

from typing import Callable

def clamp_factory(minimum=None, maximum=None) -> Callable:
    if minimum is not None and maximum is not None and minimum > maximum:
        raise ValueError
    def clamp(x):
        if minimum is not None:
            x = max(minimum, x)
        if maximum is not None:
            x = min(maximum, x)
        return x
    return clamp

at_most_100_percent = clamp_factory(maximum=1.0)

print(at_most_100_percent(-0.5)) # -0.5
print(at_most_100_percent( 0.8)) #  0.8
print(at_most_100_percent( 0.9)) #  0.9
print(at_most_100_percent( 1.0)) #  1.0
print(at_most_100_percent( 1.1)) #  1.0
print(at_most_100_percent( 1.2)) #  1.0
1 Like

Yes, it would be a different topic (or two, if math.integer gets its own Recipes), and perhaps that discussion should happen in Documentation instead of Ideas (although visibility is better here). And yes, we need multiple features to make it plural Recipes :wink:

I recall that the discussion for math.integer had more candidates. That could be a starting point.

And I agree Recipes isn’t a usual incubator for ‘real’ library functions. But it happens once in a blue moon, e.g. itertools.batched iirc.

If this is a proper way to continue, I could make two topics, either here or in Documentation.

The clamp_factory is similar to @AndersMunch’s bounds, right? And the single-sided clamping is equivalent to a half-bounded interval then (I.e. other side unbounded).

One thing about the sorted implementation is that it should always work even if you you put the min in the max and vice versa because it sorts them.

clamped = sorted((minimum, maximum, value))[1]

Or instead of throwing an error like in your original code, you can sort them via a temporary tuple:

minimum, maximum = sorted((minimum, maximum))

No problem. If they call it the wrong way, they still get the correct result.

And the itertools doc does say “A secondary purpose of the recipes is to serve as an incubator”.

1 Like

Not sure if mentioned yet, but this is in boltons, which is already the go-to utilities library for many projects (they say). And the previous discussion is here, but it never went anywhere. I cite the “not every three-line function should be in the standard library” idiom and the abundance of third-party options.

Regarding the argument order [(min, value, max) vs (value, min, max)], this would catch many incorrect usages:


percentage = clamp(raw, (0, 100))

limits = (vmin, vmax)
clamped1 = clamp(value1, limits)
clamped2 = clamp(value2, limits)

non_negative = clamp(value, (0, None))