One built-in does the whole job. The second argument is the part people leave out and then get bitten by.
numbers = [1, 3, 4, 5, 6] print(next(n for n in numbers if n % 2 == 0)) # -> 4 print(next((n for n in numbers if n > 100), None)) # -> None print(next(n for n in numbers if n > 100)) # StopIteration! # The generator is lazy: it tests 1, then 3, then 4 - and stops. # 5 and 6 are never looked at.
- A generator expression does not build a list
It produces values one at a time, on demand. next() asks for exactly one, so the condition is tested until something matches and then no further. The list-comprehension version, [n for n in numbers if ...][0], tests everything first and then throws almost all of it away.
- The second argument to next() is the default
Without it, an empty generator raises StopIteration — an error that is confusing in the middle of ordinary code because it usually means 'a loop ended'. With it, you get your fallback and no exception. Note it needs brackets round the generator once there are two arguments.
- Short-circuiting is why laziness matters here
For five numbers this is a curiosity. For a generator reading a 2GB log file line by line, stopping at the first match means you read a few kilobytes instead of two gigabytes. Same code, entirely different program.
You want one value out of a sequence of candidates, and you want to stop looking once you have it. Which built-in pulls a single value from something that produces them lazily?
A generator expression — (item for item in items if predicate(item)) — produces matches one at a time without building a list. next() takes the first one.
next(generator, default) returns the first value the generator yields, or the default if it yields nothing at all. That is the entire function: one generator expression, one next, and the default passed straight through. Do not add `or default` anywhere.
def first_match(items, predicate, default): return ____( (item for item in items if ____(item)), ____, )
def first_match(items, predicate, default):
return next((item for item in items if predicate(item)), default)
With a single argument, next(x for x in items if ...) is legal — Python allows a bare generator expression as the only argument. Add a second argument and it becomes a syntax error unless the generator is parenthesised. Worth knowing before you meet it as a confusing error message at the wrong moment.
Two of the cases here exist to prove the predicate stops being called. That matters most when the predicate is expensive — a database check, an API call, a regex over a long string — or when items is itself lazy. [item for item in items if predicate(item)][0] returns the same answer and does all the work anyway.
`return found or default` would look equivalent and quietly break for every falsy match — 0, empty string, empty list. That is the same bug as the deep-get exercise, in a different shape, and it is the single most common way this function gets written wrong.
Every version below is correct and verified against the same cases by the build. They are here because knowing which one to reach for is the part that takes judgement, and the trade-off is stated rather than left for you to guess.
def first_match(items, predicate, default):
for item in items:
if predicate(item):
return item
return defaultGenuinely fine, short-circuits exactly the same way, and easier for a beginner to read. Prefer it the moment the condition needs a comment, or when you want to do something else on the way past.
def first_match(items, predicate, default):
return next(filter(predicate, items), default)Shortest of the lot, and lazy too. It reads well precisely because the predicate is already a function — the moment the condition becomes an inline lambda, the generator expression is clearer than filter(lambda x: ..., items).
If you need all the matches, this is the wrong tool — use a comprehension and take the length, rather than calling this in a loop. If items is a dict and you are matching on the key, you already have a lookup and should not be scanning at all. And if the answer needs to say *why* nothing matched, a default cannot carry that: return a richer result or raise, so the caller can tell 'no match' from 'the default happened to be the answer'.
- Return the index alongside the item so a caller can modify the list in place, and decide what the index should be when nothing matched.
- Write last_match without reversing the whole list first, and notice why that one cannot short-circuit at all.
- Make default lazy — a function called only when nothing matches — so an expensive fallback is not computed on every successful call.