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!

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast Web Applications.


I'm available for hire as a freelance web developer, so you can take advantage of my more than a decade of experience working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm an avid writer and educator who takes pride in breaking technical concepts down into the simplest possible terms.

Continue Reading?

Here are a few more Articles for you ...


The Built-In all() Function

Learn how to use the built-in all() function in Python for boolean logic, with examples and different implementations.

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

How to Return Two Values from a Python Function

Returning multiple values from a function using tuples. Understand the syntax and how to write correct type annotations.

By Christoph Schiessl on Python

Web App Reverse Checklist

Ready to Build Your Next Web App?

Get my Web App Reverse Checklist first ...


Software Engineering is often driven by fashion, but swimming with the current is rarely the best choice. In addition to knowing what to do, it's equally important to know what not to do. And this is precisely what my free Web App Reverse Checklist will help you with.

Subscribe below to get your free copy of my Reverse Checklist delivered to your inbox. Afterward, you can expect one weekly email on building resilient Web Applications using Python, JavaScript, and PostgreSQL.

By the way, it goes without saying that I'm not sharing your email address with anyone, and you're free to unsubscribe at any time. No spam. No commitments. No questions asked.