AlgoMaster Logo

min, max, sum

High Priority20 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

min, max, and sum are three of Python's most-used built-ins. Each one collapses an iterable into a single number, and each one has a key= argument that turns it into something much more powerful than the basic definition suggests. This lesson covers the basic forms, the key and default arguments, what sum does with strings and custom objects, and the patterns where these built-ins beat writing the loop by hand.

min and max: Two Calling Forms

Both min and max accept either an iterable or two-or-more positional arguments. Same function, two shapes.

The iterable form is the common one: a list, tuple, set, generator, or anything else that can be iterated. The positional form is a shortcut for "the smaller of these two or three values", which shows up in clamping code like min(quantity, stock_left) to cap a cart quantity at the available stock.

One important rule: the two forms can't be mixed. min([1, 2, 3], 4) doesn't compare a list with a number element-wise; it compares the list and the number directly, which in Python 3 raises TypeError.

To take the smaller of "everything in the list" and "some other value", unpack the list: min(*[1, 2, 3], 4) works because it expands the list into separate positional arguments.

Both min and max are O(n) and single-pass. They walk the iterable once, holding onto the running extreme. For the top three or top ten, use heapq.nsmallest and heapq.nlargest, which are O(n log k) and avoid a full sort.

The key Argument

By default, min and max compare items directly. That works for numbers and simple strings. With richer data, they need to be told what to compare, which is what key does. The mechanic is identical to sorted: key is a function that takes one item and returns the value to use for comparison.

Two structural points. The returned value is the whole product dict, not just the price or rating. The key is only used for comparison; the original item is what's returned. This is the "argmax" pattern: instead of finding the maximum value, the call finds the item that produced the maximum value. In other languages this is a separate function; in Python, key does the work.

For the common case of "pull this field out of a dict" or "pull this attribute off an object", the operator module helpers work just as well here:

itemgetter("price") and attrgetter("price") are both implemented in C, so they run a little faster than the equivalent lambda. The bigger win is readability: key=itemgetter("price") states what it does without requiring the reader to parse a lambda body.

A third helper, less common, is operator.methodcaller. It builds a function that calls a named method on each item.

methodcaller("lower") returns a function equivalent to lambda s: s.lower(). For most code, the lambda or a plain key=str.lower reads as well.

The argmax Pattern

A common idiom: find the item that scores highest on some metric. This is "argmax" (argument of the max) in math, and max plus key does it directly.

Any "best by X" query fits this pattern. Top customer by orders, cheapest in-stock item, longest review, highest-margin product, the cart closest to a free-shipping threshold. They're all max(items, key=...) or min(items, key=...) with the right key function.

To get the score alongside the item, pull it out after the fact, or use a tuple-returning generator:

The key function can be any expression. It doesn't have to read a single field. Here it multiplies two of them, which fits the case where "best" depends on a derived quantity.

For multi-key tie-breaking, return a tuple from the key function. Tuples compare element by element, exactly like in sorted:

The Webcam wins on rating. If two items tied on rating, the tuple comparison would fall to the second element, where the negated price means "cheaper is better" within a tie.

The default Argument

min, max, and sum differ on what they do with an empty iterable. sum returns its start value (defaulting to 0). min and max raise ValueError.

Python 3.4 added a default argument to min and max for this case. Pass it and the empty iterable returns the default instead of raising.

This is the cleanest way to handle "find the most expensive item, or zero if the cart is empty" without a defensive if. It also works with a generator expression and a key function together, which is where the alternative (a try/except around the call) gets awkward.

The generator filters out the zero-stock items, leaving nothing. Without default=None, this would raise ValueError. With it, the call returns None and the rest of the code can handle it naturally.

default is checked once, only when the iterable turns out to be empty. There's no per-element overhead. Use it liberally; it's the right pattern for any min/max call where an empty input is possible.

sum: How It Actually Works

sum(iterable, start=0) walks the iterable and accumulates a running total starting from start. The mechanic is start + first + second + third + ..., left to right.

start is also positional, so sum(prices, 5.00) works the same way. It's the closest thing sum has to a key argument, except start controls the initial value, not how items are read. For "sum after transforming", combine sum with a generator expression:

The generator yields one line total per product, and sum adds them up. This is the common pattern: a generator expression for the per-item math, sum to collapse the results. It reads in the same order as the spoken description of the calculation.

sum works on any iterable of values that support +. That includes ints, floats, fractions, decimals, and any custom class that defines __add__. It does not include strings, by deliberate design.

The error message is unusually helpful. sum would technically work on strings (because "a" + "b" is valid), but it would be quadratic: each + builds a new string by copying everything that came before, so summing N strings costs O(N²) time. The built-in catches the mistake early and points to "".join(strings), which is O(N) and the right tool for joining.

join is a string method, and the string it's called on becomes the separator. The argument is the iterable of strings to join. This is the canonical Pythonic way to build a string from pieces, and it's what sum would do if it didn't refuse on principle.

sum(strings) is O(N²) when it works; "".join(strings) is O(N). The built-in refusing to sum strings exists specifically to push toward the linear-time alternative. The same advice applies to any "concat in a loop" pattern: build a list first, then join once at the end.

sum with Non-Strings and Custom Objects

Lists and tuples can be summed, with a caveat about the start argument. The default start=0 is an int, and 0 + [1, 2] raises TypeError, so an empty list (or tuple) must be passed as the start when summing lists.

That works, but it's O(N²) for the same reason as strings: each + builds a new list by copying. For flattening a list of lists, use itertools.chain.from_iterable(pairs) and wrap in list(...), which is linear.

For a custom class, define __add__ and sum works on instances of the class automatically. Consider a Money class that tracks an amount and a currency:

The __radd__ and the other == 0 check are the two pieces that make sum work without forcing the caller to pass a starting Money(0). The first iteration computes 0 + Money(29.99), which Python tries as int.__add__(0, Money(29.99)) (fails, returns NotImplemented), then falls back to Money.__radd__(Money(29.99), 0), which returns the Money instance directly. From then on, each step is Money + Money, which works normally. To skip the __radd__ step, pass start=Money(0) and only define __add__.

For floats specifically, a sharper tool exists when accuracy matters.

math.fsum (Python 3.0+) uses a tracking algorithm that compensates for floating-point rounding error. For sums of many floats where small errors would accumulate, fsum is the right choice. For cart totals with a handful of items, plain sum is fine. The cost of fsum is higher than sum, so use it only when the precision matters.

sum over floats can accumulate small errors that grow with the input size. math.fsum is slower per item but produces the mathematically correct result regardless of input size. For money, the safest path is to store amounts as integer cents and sum those.

Combining with Comprehensions and Generators

The three built-ins pair well with a generator expression. The generator does the per-item transformation; min, max, or sum collapses the result.

Three different questions, three single-line answers, all reading in the order the questions would be asked. The generator expressions filter and transform; the built-ins reduce. This is the standard shape of Python data work.

Prefer a generator expression to a list comprehension inside these built-ins. sum([x * 2 for x in items]) builds an intermediate list first, then sums. sum(x * 2 for x in items) streams one value at a time, with no intermediate allocation. Same answer, less memory.

The diagram shows the shape of the pipeline. Start with a source, pipe it through a generator that does the per-item work, then collapse with the right built-in. No intermediate lists, one pass over the data, and the intent is on a single line.

Common Patterns

A handful of patterns cover most uses of these three built-ins.

Pattern 1: Clamp a value between two limits

The nested call reads "the larger of low and (the smaller of value and high)". It pins the value into the [low, high] range with no if statements.

Pattern 2: Weighted sum (price times quantity)

The generator yields each line total; sum adds them. For a weighted average instead of a weighted sum, divide by sum(item["qty"] for item in cart).

Pattern 3: Find the item closest to a target

The key is the distance from the target. min finds the item with the smallest distance, which is the closest match. This is also the building block for "snap to nearest" logic in pricing tiers, discount thresholds, or shipping bands.

Pattern 4: Top-K instead of top-1

For the three best items instead of the single best, use heapq rather than calling max repeatedly:

heapq.nlargest(k, items, key=...) is O(n log k), which beats sorting the whole list when k is small. There's a matching heapq.nsmallest for the other direction.

Pattern 5: When NOT to use these built-ins

Some shapes look like a fit but aren't. The notable ones:

ShapeDon't useUse instead
Concatenating stringssum(strings, "")"".join(strings)
Flattening a list of listssum(lists, [])list(itertools.chain.from_iterable(lists))
Finding the index of the maxmax(...)max(range(len(items)), key=items.__getitem__) or items.index(max(items))
Adding many floats accuratelysum(floats)math.fsum(floats)
Counting occurrencessum(1 for x in items if cond)sum(1 for x in items if cond) is fine, or collections.Counter for multi-key

The flattening and concatenation rows are about quadratic-time traps. The float row is about accuracy. The index row is because max returns the item, not its position; getting the position requires a different formulation.

Quiz

min, max, sum Quiz

10 quizzes