min() returns the smallest item, and if that were all it did it would not be worth an article. The reason it is worth knowing well is the key argument, which turns min from ‘smallest number’ into ‘the item that scores lowest on any function you like’ - the shortest string, the cheapest product, the earliest date, the closest point. The default argument is the other half: it is what stops min() from raising ValueError when the iterable is empty, which is the one way this function actually crashes in production.
Two arguments, key and default, are the difference between reaching for min and hand-writing a loop that does the same thing worse.
Table of contents
- Two ways to call it
- key: the argument that earns its keep
- default: surviving the empty iterable
- Ties, and how min breaks them
- min vs sorting vs heapq
- How this fits the rest of the stack
- FAQ
Two ways to call it
min(3, 7, 1) # 1 - several arguments
min([3, 7, 1]) # 1 - one iterable
min("cat", "apple") # 'apple' - strings compare lexicographically
min([]) # ValueError: min() arg is an empty sequence
You either pass several values as separate arguments, or one iterable. Mixing them (min([3, 7], 1)) is a TypeError. The empty case is the trap, and the fix is the default argument below.
key: the argument that earns its keep
key is a function applied to each item to decide what to compare, without changing what gets returned.
products = [
{"name": "A", "price": 40},
{"name": "B", "price": 25},
{"name": "C", "price": 60},
]
cheapest = min(products, key=lambda p: p["price"])
# {'name': 'B', 'price': 25} - the whole dict, not just 25
words = ["pear", "fig", "banana"]
shortest = min(words, key=len) # 'fig'
This is the pattern that replaces a sort. If you only need the single smallest item, min(items, key=...) is O(n) and clearer than sorted(items, key=...)[0], which is O(n log n) and allocates a whole sorted list to throw all but one element away.
default: surviving the empty iterable
min([]) raises ValueError. In real code the iterable is often a filtered list that legitimately came out empty, so the crash is a genuine bug, not a programmer error.
prices = [p for p in products if p["price"] < 20] # might be empty
min(prices, key=lambda p: p["price"], default=None)
# returns None instead of raising when prices is empty
default is keyword-only and only allowed when you pass a single iterable. Use it whenever the iterable can be empty and you would rather have a sensible fallback than a traceback. It is the difference between a clean ‘no results’ path and a 500 error.
Ties, and how min breaks them
When two items tie on the key, min returns the first one it encountered. That is a stable, documented guarantee, not luck, and it means order matters when ties are possible.
data = [(1, "a"), (1, "b"), (2, "c")]
min(data, key=lambda t: t[0]) # (1, 'a') - first of the tied pair
If you need a tiebreaker, put it in the key by returning a tuple: key=lambda t: (t[0], t[1]) compares the first element, then the second. Tuples compare element by element, which makes multi-level sorting and min/max selection a one-liner.
min vs sorting vs heapq
- Need the single smallest?
min(items, key=...). One pass, no allocation. - Need the k smallest?
heapq.nsmallest(k, items, key=...). Faster than sorting the whole list when k is small. - Need everything in order?
sorted(items, key=...). Only pay O(n log n) when you actually need the full ordering.
Reaching for sorted(...)[0] to get one minimum is the most common over-spend here. It works, but it does far more work than the question requires.
How this fits the rest of the stack
Choosing min over a full sort is a small version of a bigger habit: match the tool to the size of the question instead of over-provisioning by reflex. That habit pays off most on infrastructure, where the reflex is to buy a bigger box rather than size the actual workload. The RunxBuild hosting calculator lays out compute, memory, database, and bandwidth as separate line items so you can size the real workload, and the RunxBuild dashboard shows what the running service actually consumes.
Useful related references:
- Python floor(): floor, int, and // on negatives
- Append to a List in Python
- Python services on RunxBuild
FAQ
How does the key argument work in Python min()?
key is a function applied to every item to decide what to compare, while min still returns the original item. min(products, key=lambda p: p[‘price’]) returns the whole cheapest product dict, not just the price. It is the same pattern as sorted’s key, and for a single minimum it is faster and clearer than sorting and taking the first element.
How do I stop min() from raising ValueError on an empty list?
Pass the default argument: min(items, key=…, default=None). When the iterable is empty, min returns the default instead of raising. It is keyword-only and only valid when you pass a single iterable. Use it any time the iterable is the result of a filter that could legitimately be empty.
Which item does min() return when there is a tie?
The first one it encounters in iteration order. This is guaranteed, so order is meaningful when ties are possible. To break ties deterministically, make the key return a tuple - key=lambda t: (t[0], t[1]) - and Python will compare the second element only when the first ties.
Is min() faster than sorting to find the smallest value?
Yes. min() makes a single O(n) pass, while sorted(items)[0] is O(n log n) and allocates a fully sorted list you then discard. For one minimum use min; for the k smallest use heapq.nsmallest; only sort when you actually need the whole sequence ordered.
Can min() compare strings or dates?
Yes. Strings compare lexicographically by Unicode code point, so min(‘cat’, ‘apple’) is ‘apple’. datetime objects compare chronologically, so min returns the earliest. For custom objects, either pass a key or implement the comparison methods; otherwise Python raises a TypeError because it does not know how to order them.