How does next((x for x in xs if p(x)), None) find the first match without scanning all of xs?
answer
- Stop as soon as you find it
- Search without building a list
- One lazy source, one fallback value
- Work is proportional to the match position
- A falsy hit can look like a miss
basics
~20 sThe filtered source is lazy, so next pulls items one at a time and stops the moment the predicate first holds. Nothing beyond the match is examined, and the default supplies a value when no item matches.
solid answer
~50 sThe argument inside the call is a lazy source that yields only matching items, and `next()` asks it for exactly one. Producing that one item forces the source to walk `xs` until the predicate first holds, then stop - so the work is proportional to the position of the match, not to the length of `xs`. The second argument matters because an unmatched search exhausts the source, which would otherwise raise `StopIteration`; the default turns "no match" into a returned value. This is why the idiom beats building a filtered list and taking index 0: that variant evaluates the predicate for every element and raises `IndexError` on no match. The one trap is a falsy match: if a real hit can be `0` or `""`, compare the result with `is None`, or use a unique `object()` sentinel as the default instead.
code
python · 7 linesservices = [
{"name": "ingest", "healthy": True},
{"name": "transcript-writer", "healthy": False},
{"name": "search-index", "healthy": False},
]
first_down = next((s for s in services if not s["healthy"]), None)
print(first_down["name"] if first_down is not None else "all 17 services up")go deeper
Recognise the shape and what it returns: the first element for which the condition holds, or the fallback if none does. Know that it stops at the first hit instead of examining the whole input.
Explain the laziness precisely - work proportional to the match position - and why the default is required to avoid StopIteration. Show the falsy-match trap and the identity check that fixes it.
Demonstrate judgement about when a linear scan is the wrong shape at all: repeated lookups want a dict built once, and a nested search turns an innocuous expression into quadratic work.
Own the codebase convention: one shared sentinel for absence, agreement on whether 'not found' returns a fallback or raises, and where a scan should become an indexed structure at the design level.
"Find the first element that satisfies a condition, or nothing if there is none" is one of the most common small searches in real code, and Python spells it as a single expression: a lazily filtered source passed to `next()`, with a fallback as the second argument. ### Why it does not scan everything The key property is laziness. The filtered source produces items on demand: it holds a cursor into `xs`, and each time something asks it for a value it pulls elements and tests the predicate until one passes, then hands that element back and freezes. `next()` asks for exactly one item, so the source runs exactly until the first hit. If the match is the second of a 17-entry dependency graph, the predicate runs twice and the remaining fifteen entries are never touched. That makes the cost proportional to the *position* of the match rather than the size of the input. The eager alternative does not share this property: ```python matches = [x for x in xs if p(x)] # tests every element first = matches[0] # IndexError when empty ``` That version evaluates `p` for all of `xs`, allocates a list you throw away, and raises `IndexError` rather than yielding a fallback. If `p` is a network call, a regex over a long line, or a parse of a locale-dependent timestamp, the difference is not cosmetic. ### Why the default is not optional When no element matches, the filtered source is exhausted and raises `StopIteration`. With a one-argument `next()` that exception reaches your code as a genuine error, and at module level it is an ugly traceback for what is really an ordinary "not found". Supplying the second argument turns the no-match case into a value, which is why the idiom is nearly always written with one. ### The falsy-match trap `None` is the conventional default, and it is wrong exactly when a legitimate match can itself be falsy. A search for the first service with zero unresolved dependencies can return `0`; a search for the first blank message body can return `""`. Written as `if found:` the code then treats a real hit as a miss. Two disciplined fixes: - compare identity, not truth: `if found is None:` - correct whenever `None` cannot itself be an element; - use a private sentinel: `MISSING = object()` as the default and `if found is MISSING:`. Each `object()` is unique, so no element can ever equal it. This is the version to use when `None` is itself a possible item. ### How it compares to a loop The explicit form is a `for` loop with a `break`, and an `else` clause on the loop for the not-found case. That is not wrong - it is more readable when the match needs several statements to compute, or when you also want the index or a count. The one-expression idiom wins when the predicate is a simple expression and the result is immediately assigned or returned: it has no mutable accumulator, no partially-initialised name before the loop, and no chance of forgetting the `else`. ### What it is not for It is a linear scan. Calling it inside another loop makes the whole thing quadratic, and if you are looking things up repeatedly by the same key, the right answer is to build a dict once and index it. It also gives you the *element*, not its position; if you need the position, that is a different tool. And because the source is consumed, calling `next()` twice on the *same* source object continues from where it stopped - which is occasionally what you want ("the first two matches") and is a bug when you thought each call restarted the search. ### A syntax detail that bites When a generator expression is the *only* argument to a call, the surrounding parentheses of the call suffice. With a second argument they do not: the expression must be parenthesised in its own right, or the code is a `SyntaxError` at compile time. So the idiom always carries the double opening parenthesis, and a candidate who has hit it once never forgets it.
- Why is a sentinel from object() sometimes better than None as the default here?Because `None` may be a legitimate element, and even when it is not, a truthiness check conflates `0`, `""` and empty containers with "not found". A module-level `MISSING = object()` is guaranteed unique - no element can be identical to it - so `found is MISSING` is an unambiguous not-found test. Use `None` only when you have checked that neither `None` nor a falsy value can be a real result, and then still test with `is None` rather than truthiness.
- What happens if you call next() twice on the same filtered source?The second call resumes from where the first stopped, giving the *second* match rather than the first again. The source is a stateful cursor, not a re-runnable query. That is useful when you deliberately want the first two matches, and a bug when the second call was meant as an independent search - in that case build a fresh source expression for each search.
- When would you prefer an explicit for/break loop instead?When the match needs several statements to compute, when you also want the index or a running count, when you want to log each rejected candidate, or when the predicate is long enough that inlining it hurts readability. The loop with a `for ... else` clause expresses the same search and stays legible; the one-expression form is for the case where the predicate fits comfortably on the line.
saying these in an interview costs you the question
- Claims the whole input is filtered before next() runs
- Uses if found: and misreads a match of 0 as no match
- Suggests building a filtered list and taking index zero
- Omits the default and lets StopIteration escape
- Expects a second next() call to restart the search
- Forgets the generator expression needs its own parentheses