Skip to main content

Soft-deprecating re.match()

Quick, without looking it up, what does re.match() do? Which of these return a match?

import re
re.match("pi", "pi")
re.match("pi", "pie")
re.match("pi", "api")
re.match("pi", "magpie")

How does it compare to re.search() and re.fullmatch()?

Whilst you’re (quickly) thinking about it, let’s introduce soft deprecation.

Soft deprecation #

Python’s backwards compatibility policy (PEP 387) introduced soft deprecation in 2023:

A soft deprecation can be used when using an API which should no longer be used to write new code, but it remains safe to continue using it in existing code. The API remains documented and tested, but will not be developed further (no enhancement).

A soft deprecation does not imply future removal of the API, nor does it issue a warning. It’s a docs-only recommendation to not use an API, ideally with a suggested replacement.

It’s a completely separate decision whether, if ever, to turn a soft deprecation into a regular “hard” deprecation (where removal may follow); soft deprecations don’t “graduate” into regular deprecations or removals.

re.match() #

Now the answer:

>>> import re
>>> re.match("pi", "pi")      # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.match("pi", "pie")     # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.match("pi", "api")     # ❌ No match
>>> re.match("pi", "magpie")  # ❌ No match
>>>

So re.match() only matches at the beginning of a string! This can be surprising: why is the start of the string special?

re.search() #

If you don’t want to anchor at the start, and want to match anywhere in the string, use re.search():

>>> import re
>>> re.search("pi", "pi")      # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.search("pi", "pie")     # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.search("pi", "api")     # ✅ Matches
<re.Match object; span=(1, 3), match='pi'>
>>> re.search("pi", "magpie")  # ✅ Matches
<re.Match object; span=(3, 5), match='pi'>
>>>

re.fullmatch() #

If you want to anchor both the start and the end, and check the entire string matches, use re.fullmatch():

>>> import re
>>> re.fullmatch("pi", "pi")      # ✅ Matches
<re.Match object; span=(0, 2), match='pi'>
>>> re.fullmatch("pi", "api")     # ❌ No match
>>> re.fullmatch("pi", "pie")     # ❌ No match
>>> re.fullmatch("pi", "magpie")  # ❌ No match
>>>

Introducing re.prefixmatch() #

Because of the surprising half-anchored behaviour, we’ve introduced a new alias for re.match() in Python 3.15, named re.prefixmatch():

Quoting from the Zen Of Python (python3 -m this): “Explicit is better than implicit”. Anyone reading the name prefixmatch() is likely to understand the intended semantics. When reading match() there remains a seed of doubt about the intended behavior to anyone not already familiar with this old Python gotcha.

Soft-deprecating re.match() #

And with a more explicit replacement, we’ve soft-deprecated re.match() in Python 3.15:

We do not plan to remove the older match() name, as it has been used in code for over 30 years. It has been soft deprecated: code supporting older versions of Python should continue to use match(), while new code should prefer prefixmatch().

Use re.prefixmatch() if you only really meant to use the half-anchor; otherwise use re.search() or re.fullmatch().

Comparison #

FunctionStart anchorEnd anchorAdded inWith special characters
re.search()1.5re.search("pi", string)
re.match()1.5re.search("^pi", string)
re.search(r"\Api", string)
re.prefixmatch()3.15re.search("^pi", string)
re.search(r"\Api", string)
re.fullmatch()3.4re.search("^pi$", string)
re.search(r"\Api\z", string)

The functions without special characters are generally a bit faster.

See also #


Header photo: Double-exposure bike and pedestrian stencils (CC BY-NC-SA 2.0 Hugo van Kemenade).