Both take two arguments here and pick one of them. Read the last two lines together — that combination is the whole exercise.
print(min(7, 10)) # -> 7 the smaller of the two print(max(7, 10)) # -> 10 the larger print(max(0, min(7, 10))) # -> 7 inside the range, untouched print(max(0, min(-3, 10))) # -> 0 min let -3 through, max lifted it print(max(0, min(99, 10))) # -> 10 min capped it at 10
- min caps the top, max lifts the bottom
min(value, high) can only ever make the number smaller, so it enforces the ceiling. max(low, ...) can only make it larger, so it enforces the floor. Applying one and then the other gives you both limits with no branching at all.
- Both are already inclusive
min(10, 10) is 10 and max(0, 0) is 0, so a value sitting exactly on a bound passes through unchanged without any special handling. The contract’s inclusive rule falls out of the tools rather than being coded.
- They only pick — they never compute
min and max return one of the arguments you gave them, so an int stays an int and a float stays a float. Nothing is rounded, converted or recalculated on the way through.
Two limits, applied one after the other. Which built-in stops a number going above a value, and which stops it going below one?
min(value, high) enforces the ceiling. Feeding that result into max(low, ...) then enforces the floor. Together they need no if at all.
Guard first: if low > high, raise ValueError. Then return max(low, min(value, high)). That is the entire function — two lines of guard and one of answer.
def clamp(value, low, high): if low > ____: raise ValueError(...) return ____(low, ____(value, high))
def clamp(value, low, high):
if low > high:
raise ValueError(f"low ({low}) cannot be greater than high ({high})")
return max(low, min(value, high))
Call clamp(5, 10, 0) without it and you get 10 — a real-looking number, no error, and no clue that the bounds were swapped. That is far worse than a crash, because it travels. Whenever a compact expression cannot detect its own bad input, the check has to go in front of it.
if value < low: return low; elif value > high: return high; else: return value is correct and three times as long, with three comparisons to get the right way round instead of none. Both read fine; the min/max version reads as *what* rather than *how*, and that is usually the better default.
The contract says a value equal to a bound comes back unchanged, which is what min and max already do. Exclusive bounds would need a different function and a different name — and would raise the question of what clamp(10, 0, 10) should even return. Inclusive avoids that entirely.
Do not clamp a value that has no business being out of range. If a percentage arrives as 150, clamping it to 100 hides a bug upstream and ships wrong data quietly; validating and refusing surfaces it while somebody can still fix the cause. Clamp for presentation and geometry — screen coordinates, volume sliders, progress bars — where out-of-range genuinely means 'as far as it goes'. Validate for data.
- Make low and high optional so clamp(value, low=None, high=0) limits only one side, and decide what the signature should look like so a caller cannot misread it.
- Write clamp_index(items, index) that keeps an index inside a list, then notice it is this function with low fixed at 0 and high derived from len().
- Add a version that reports whether it changed anything, so a caller can log 'value was out of range' — and think about what shape that return value should take.