Quick summary
Summarize this blog with AI
“First match” can mean three different results: a substring offset, a regular-expression match object, or an item from an iterable. Choose the result contract first. All examples run on Python 3.12.14 with the standard library.
| Need | Tool | No match |
|---|---|---|
| Substring position | text.find(needle) | -1 |
| Pattern and captured text | re.search(pattern, text) | None |
| First qualifying item | next(generator, default) | The chosen default |
Treat position zero as a successful match
text = "Python and SQL"
for needle in ["Python", "SQL", "R", ""]:
position = text.find(needle)
print(repr(needle), position, position != -1)
print("Python" in text)
Expected output:
'Python' 0 True
'SQL' 11 True
'R' -1 False
'' 0 True
True
if text.find(...) is wrong for existence: zero is falsy, while -1 is truthy. Use in when you only need membership, or compare the offset with -1. Empty text is a substring at position zero under this contract. Search is case-sensitive; any case normalization must be deliberate. Python string offsets count Unicode code points, not encoded bytes or necessarily whole displayed grapheme clusters.
Inspect the regex Match instead of treating it as an index
import re
text = "id=17; id=42"
match = re.search(r"id=(\d+)", text)
if match is not None:
print(match.group(0), match.group(1), match.span())
print(re.search(r"name=(\w+)", text) is None)
print(re.match(r"id=", "prefix id=17") is None)
print(re.fullmatch(r"\d+", "17x") is None)
Expected output:
id=17 17 (0, 5)
True
True
True
search looks for the first matching position, match starts at the beginning, and fullmatch requires the entire input. The group text and span are separate properties. Regex alternation and greediness still determine what matches at that position; “first” does not mean shortest. For a literal user-provided substring, prefer string methods or escape it with re.escape rather than interpreting it as a pattern. Bound untrusted input and avoid patterns with pathological backtracking.
Use a sentinel when None can itself be a valid item
MISSING = object()
values = [None, 0, 7, 9]
first = next((value for value in values if value is None), MISSING)
print(first is None, first is MISSING)
print(next((n for n in [] if n > 5), MISSING) is MISSING)
print(next((pair for pair in enumerate([2, 8, 8]) if pair[1] > 5), None))
stream = iter([2, 8, 9])
print(next((n for n in stream if n > 5), None))
print(list(stream))
Expected output:
True False
True
(1, 8)
8
[9]
The generator stops at the first qualifying item and leaves the source iterator after that item. A reusable list remains intact; a generator or file iterator is consumed. If absence is exceptional, omit the default and handle StopIteration at the correct boundary. If you need both position and value, return an enumerate pair as shown. An empty input is a normal no-match case, not an indexing error.
Define ordering and missing-data rules
A first matching record depends on input order. Sort under a documented key first if “first” means earliest timestamp, and include a tie-breaker when one winner is required. Do not convert an unordered set to a list and call its first element meaningful. For a missing dictionary field, decide whether the record is invalid or should be skipped; an accidental default can hide bad data. Test a match at position zero, no match, repeated matches, empty input, and a valid item equal to your proposed default.