Match.span
Positions, not text: m.start() and m.end() are the slice bounds of the match, so string[m.start():m.end()] == m.group(). Pass a group number or name for a group; an unmatched group reports -1.
Common call
m.span(1)
Returns
(4, 7) — start and end of group 1
Replaces
str.find after the fact
Watch out
end is exclusive; unmatched groups give -1, which is a valid (wrong) slice index
Match.start([group]) / Match.end([group]) / Match.span([group])
→ int / int / tuple[int, int]
Demo
Live evaluation
Positions of a group (0 = whole match). Type a number or a name.
Try:
Inputs
patternstra regular expression
textstrtext
groupint | strnumber or name
Code
import re m = re.search('(?P<key>\\w+)=(?P<val>\\w+)', 'set mode=fast now') (m.start(0), m.end(0), m.span(0))
Result
(4, 13, (4, 13))
end() is exclusive, so text[start:end] is exactly the match and end - start its length. A group that did not participate gives -1 for both ends. The \b case is an empty match: start and end are equal (0, 0).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| group | int | str | no (0) | Group number or name; 0 (the default) is the whole match. |
Return value
int / int / tuple[int, int] — Indexes into the original string; -1 (or (-1, -1)) for a group that did not participate.
Common patterns
Cut the string around a match
Everything before and after the first match.
import re m = re.search(r'\s*#', line) code = line[:m.start()] if m else line
Collect all positions
finditer + span for every match.
import re positions = [m.span() for m in re.finditer(r'\bTODO\b', source)]
Continue scanning after a match
m.end() is the next pos for Pattern.match / Pattern.search.
import re pos = 0 while m := WORD.search(text, pos): handle(m.group()) pos = m.end()
Examples
1. Start and end
import re
m = re.search(r'\d+', 'order 66')
(m.start(), m.end())
Returns
(6, 8)2. span() is both
import re
re.search(r'\d+', 'order 66').span()
Returns
(6, 8)3. Span of a group
import re
re.search(r'(\w+)@(\w+)', 'mail ada@home').span(2)
Returns
(9, 13)4. By group name
import re
re.search(r'(?P<user>\w+)@(?P<host>\w+)', 'mail ada@home').start('host')
Returns
95. Unmatched group: -1
import re
re.match(r'(a)(b)?', 'a').span(2)
Returns
(-1, -1)6. Slicing reproduces the match
import re
s = 'order 66'
m = re.search(r'\d+', s)
s[m.start():m.end()] == m.group()
Returns
TruePitfalls
1. Slicing with the -1 of an unmatched group
A skipped group reports -1, and s[:-1] is a valid slice — you silently get the wrong text. Check the group first.
s[:m.start(2)]
import re s = 'w: 40' m = re.search(r'(\d+)(px)?', s) s[:m.start(2)]
'w: 4'
check for -1
import re s = 'w: 40' m = re.search(r'(\d+)(px)?', s) s[:m.start(2)] if m.start(2) != -1 else s
'w: 40'
2. Positions relative to pos
With Pattern.search(text, pos) the positions are still indexes into the whole string, not into text[pos:].
assume relative
import re m = re.compile(r'\d').search('ab12', 2) 'ab12'[2:][m.start()]
IndexError: string index out of range
absolute index
import re m = re.compile(r'\d').search('ab12', 2) 'ab12'[m.start()]
'1'
When to use
Use it
- You need where a match is: to slice, highlight, or continue scanning
- Positions of individual groups
Reach for something else
- Just the text → group()
- Positions of every match → finditer + span()
Notes
CPython impl
match_start / match_end / match_span in Modules/_sre/_sre.c read the stored marks; -1 means the group has no mark
Empty matches
For an empty match start == end; for a group that matched the empty string too
Indexes
Always indexes into Match.string (code points of the str), independent of pos
FAQ
m.start() gives the index of the first character, m.end() the index after the last one, m.span() both — where m = re.search(pattern, text).