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 largest 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 there is no need for it. 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.

Built-in mapping support 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 largest. 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):
...     self.name = name
...     self.height = height
...
>>> min(Person("Alice", 171), Person("Bob", 180), Person("Charly", 169))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Person' and 'Person'
>>> result = min(Person("Alice", 171), Person("Bob", 180), Person("Charly", 169), key=lambda p: p.height)
>>> result.name
'Charly'
>>> result = max(Person("Alice", 171), Person("Bob", 180), Person("Charly", 169), 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() always 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!

Ready to Learn More Web Development?

Join my Mailing List to receive two articles per week.


I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


Function Definition with Position-Only Parameters

Learn about positional parameters in Python and a special syntax that allows functions to declare certain parameters as position-only.

By Christoph Schiessl on Python

The Built-in sum() Function

In this article, we explore Python's built-in sum() function, its parameters, and some extreme use cases it wasn't even designed for.

By Christoph Schiessl on Python

The Built-in any() Function

Learn how to use the built-in any() function in Python to determine if any element in an iterable is True, with implementation and performance insights.

By Christoph Schiessl on Python

Christoph Schiessl

Christoph Schiessl

Independent Consultant + Full Stack Developer


If you hire me, you can rely on more than a decade of experience, which I have collected working on web applications for many clients across multiple industries. My involvement usually focuses on hands-on development work using various technologies like Python, JavaScript, PostgreSQL, or whichever technology we determine to be the best tool for the job. Furthermore, you can also depend on me in an advisory capacity to make educated technological choices for your backend and frontend teams. Lastly, I can help you transition to or improve your agile development processes.