The Built-In min()
and max()
Functions
by Christoph Schiessl on Python
Python's built-in min()
and max()
functions are interesting to study because they appear to be overloaded. This means the identifiers min()
and max()
effectively refer to two functions each, and their positional parameters determine which variant gets called. I'm saying "appear to be overloaded" because Python doesn't support actual function overloading in the sense that Java does, for instance. However, Python functions are very flexible in handling their parameters, so it's possible for a function to inspect its parameters and exhibit different behavior based on that.
Putting the parameter handling aside for a moment, both functions do precisely what their names suggest. min()
returns the smallest, and max()
returns the greatest value given to it. Otherwise, their behavior is identical — what you learn about min()
transfers to max()
and vice versa.
With one iterable
positional parameter
The most common case is probably to call min()
/max()
with a single parameter: an object implementing the iterable
protocol (e.g., a list
object).
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> min([2])
2
>>> max([2,3])
3
>>> min([1,2,3])
1
>>> min(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>> max(set())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: max() iterable argument is empty
If you call the functions with a single positional parameter that does not implement the iterable
protocol, they raise a TypeError
. Furthermore, if you call the functions with an empty iterable
(e.g., an empty set
), they raise a ValueError
.
Both cases are not surprising because what else could the functions do? For the former case, the exception is really the only sound option. For the latter case, the empty iterable
, the reasonable alternative would have been to return None
, but the Python developers made the design decision to raise an exception.
Empty iterable
objects are very common, so min()
/max()
take an optional keyword-only parameter named default
to provide a return value for those cases. Hence, the default
parameter avoids the ValueError
, which they would otherwise raise if the iterable
object is empty.
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> min([], default=2)
2
>>> min([3], default=2)
3
>>> min([1,3], default=2)
1
>>> max([], default=2)
2
>>> max([1], default=2)
1
>>> max([1,3], default=2)
3
The default
parameter is ignored if the iterable
is not empty.
With multiple positional parameters
The second variant uses two or more positional parameters. In this case, there is no default
parameter because it is unnecessary. Given that at least two positional parameters are required, it can never be unclear what the function is supposed to return.
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> min(2, 3)
2
>>> max(1, 2, 3)
3
>>> min(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
A single positional parameter triggers the first variant again, meaning min()
/max()
, expect an object implementing the iterable
protocol.
Mapping with the key
parameter
Both variants of min()
/max()
support an optional keyword-only parameter called key
, which must be a callable
object and accept a single parameter. This object is then called with the given values to map them to some other value for the sake of the comparisons that determine which value is the smallest or the greatest. Using a lambda
function for the key
parameter is common practice. Note that min()
/max()
still return one of the original values, and the key
parameter only affects the decision of which of these values to return.
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> min([1,2,3], key=lambda x: -x)
3
>>> max(1,2,3, key=lambda x: -x)
1
Finally, key
is also helpful with custom objects that are not comparable but must first be mapped to some other value with a defined ordering.
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Person:
... def __init__(self, name: str, height: int) -> None:
... self.name = name
... self.height = height
...
>>> people = [Person("Alice", 171), Person("Bob", 180), Person("Charly", 169)]
>>> min(people)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Person' and 'Person'
>>> result = min(*people, key=lambda p: p.height)
>>> result.name
'Charly'
>>> result = max(people, key=lambda p: p.height)
>>> result.name
'Bob'
Precedence of Values
Lastly, I want to point out that min()
/max()
always return the first minimal/maximal value if there are multiple ones. To prove this, we can use plain objects as input values and the key
parameter to map them all to the same value. Hence, there are multiple minimal/maximal values. Finally, we can use the is
operator to compare object identities to prove that min()
/max()
indeed return the first minimal/maximal value in such situations.
Python 3.12.3 (main, Apr 20 2024, 16:22:09) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> values = [object(), object()]
>>> assert values[0] is not values[1]
>>> min(values, key=lambda _: 0) is values[0]
True
>>> min(*values, key=lambda _: 0) is values[0]
True
>>> max(values, key=lambda _: 0) is values[0]
True
>>> max(*values, key=lambda _: 0) is values[0]
True
Thank you very much for reading! I hope you found this article interesting, and I look forward to seeing you again soon!