error-message-matching (SMP507)¶
Flags an except handler that tells exceptions apart by their text, by comparing or searching str(exc), exc.args or
exc.message.
Why is this bad?¶
The text of an exception is a message for people, and it changes with the wording, the translation and the library version. A check on the text stops matching without any error, and the handler then takes the wrong branch. A specific exception type says what happened, and it does not change with the text.
Example¶
1try:
2 data = load(path)
3except OSError as exc:
4 if "timed out" in str(exc):
5 data = load(path, retries=3)
6 else:
7 raise
1try:
2 data = load(path)
3except TimeoutError:
4 data = load(path, retries=3)
What it does¶
Each case is checked inside the body of an except ... as exc: handler. Test modules are left alone, since their
asserts may check the messages the code raises.
Active on¶
Comparisons and
intests with the text: an operand that isstr(exc),exc.args,exc.args[N]orexc.message, in aniforelifcondition or anywhere else in the handler.Example: reported
1def run(exc_source): 2 try: 3 exc_source() 4 except OSError as exc: 5 if "timed out" in str(exc): 6 return None 7 if exc.args[0] == "x": 8 return "" 9 return exc
strmethods that search the text:startswith,endswith,find,indexandcountcalled onstr(exc).Example: reported
1def run(exc_source): 2 try: 3 exc_source() 4 except OSError as exc: 5 if str(exc).startswith("timed"): 6 return None 7 return exc
refunctions matching the text:re.search,re.matchandre.fullmatchwithstr(exc)as an argument.Example: reported
1import re 2 3 4def run(exc_source): 5 try: 6 exc_source() 7 except OSError as exc: 8 if re.search("gone", str(exc)): 9 return None 10 return exc
matchsubjects: amatchstatement whose subject is one of the text forms.Example: reported
1def run(exc_source): 2 try: 3 exc_source() 4 except OSError as exc: 5 match str(exc): 6 case "missing": 7 return None 8 return exc
Exceptions¶
The text in logging, formatting or re-raising: a message built for a person does not pick a branch.
Example: not reported
1import logging 2 3logger = logging.getLogger(__name__) 4 5 6def run(exc_source): 7 try: 8 exc_source() 9 except OSError as exc: 10 logger.error("failed: %s", str(exc)) 11 print(f"failed: {exc}") 12 raise
Attributes that hold data:
exc.errnoand other fields are values, not text.Example: not reported
1def run(exc_source): 2 try: 3 exc_source() 4 except OSError as exc: 5 if exc.errno == 2: 6 return None 7 raise
assertstatements that name the exception: Ruff’s pytest-assert-in-except (PT017) reports anassertin anexceptbody that names the exception, so this rule leaves the comparison inside it to Ruff while the project’s Ruff configuration selects that rule. Ruff’s own suppressions of it, throughper-file-ignoresornoqa, are respected too.Example: not reported
pyproject.toml1[tool.ruff.lint] 2select = ["PT017"]
1def run(exc_source): 2 try: 3 exc_source() 4 except OSError as exc: 5 assert "gone" in str(exc)
Comparisons of two exceptions’ text: every operand is the text of an exception, such as
str(exc) == str(other). Both sides are text the runtime produces, so no fixed wording is written into the code that could drift. A name is an exception when each of its bindings in scope is anexceptclause name, or a parameter annotated with an exception type: a builtin one, a class of the run or one it imports from there, alone, in a union withNone, inOptional[...], or in a string annotation of those.Example: not reported
1def replayed(target, error: OSError): 2 try: 3 target() 4 except OSError as replay_error: 5 return type(replay_error) is type(error) and replay_error.args == error.args
Test modules: tests may assert on the messages the code raises.
Example: not reported
test_errors.py1def test_load(): 2 try: 3 load() 4 except OSError as exc: 5 assert "gone" in str(exc)
pytest.raiseswithmatch: thematchargument ofpytest.raisesis a pattern the test expects, not a branch.Example: not reported
1import pytest 2 3 4def test_load(): 5 with pytest.raises(OSError, match="gone"): 6 load()