Your withdraw function checks its input with assert:
from dataclasses import dataclass
@dataclass
class Account:
balance: int
def withdraw(account: Account, cents: int) -> None:
assert cents > 0, "amount must be positive"
account.balance -= cents
account = Account(balance=500)
withdraw(account, -100)
print(account.balance)
$ python bank.py
AssertionError: amount must be positive
$ python -O bank.py
600
The -O flag tells Python to optimize, and Python then drops every assert
statement before the code runs. Setting the environment variable
PYTHONOPTIMIZE=1 does the same. The withdrawal of -100 cents goes through
and adds 100 cents to the balance.
Raise an exception when the check must run
An if that raises runs under every flag:
def withdraw(account: Account, cents: int) -> None:
if cents <= 0:
raise ValueError("amount must be positive")
account.balance -= cents
Python also skips the expression inside a dropped assert. So never put work
the program needs inside one, as in assert save(record).
An assert still fits a condition that only a bug in your own code could
break. For example, your cache should never hold more items than its size
limit. The assert tells the reader what you expect, and it catches the bug
while you develop.
Let Ruff flag assert in app code
Ruff is a linter, a tool that flags risky or messy code without running it.
Its rule S101 flags every assert. That is what you want in app code. In
tests it is noise, because assert is how a pytest test checks its result.
Turn the rule on for the project and off for the tests, in
pyproject.toml:
[tool.ruff.lint]
extend-select = ["S101", "RUF100"]
[tool.ruff.lint.per-file-ignores]
# pytest tests check results with assert.
"tests/**/*.py" = ["S101"]
extend-selectadds rules to the ones Ruff already checks.selectreplaces that list, so use it only when you mean to.per-file-ignoresturns rules off for the files that match a pattern. Change the pattern if your tests live somewhere else.RUF100flags a# noqacomment that no longer hides any finding, so you can delete it.
If these tables already exist, add the lines to them. TOML does not allow the
same table header twice. Now ruff check . reports the assert in
withdraw and accepts the ones in your tests:
S101 Use of `assert` detected
--> bank.py:10:5
pytest keeps its own asserts under -O
pytest rewrites the assert statements in your test files before they run.
That is how a failure can show both sides of a comparison, as in
assert 600 == 500. The rewritten checks still run under python -O, but
the asserts in your app code are gone, and pytest warns you:
PytestConfigWarning: assertions not in test modules or plugins will be ignored because assert statements are not executed by the underlying Python interpreter (are you using python -O?)
So an assert in a test is safe to keep. An assert in the code under test
is the one to replace.
Pick the smallest exemption that fits
- For one intentional
assertin app code, add# noqa: S101to that line, with a comment that says why. - For a folder whose code has a different job, such as tests or generated code, use a per-file ignore.
- For a rule your team never wants, turn it off for the whole project.
Check your understanding
Ruff reports S101 on twenty lines of tests/test_bank.py. Should you add # noqa: S101 to each one?
Show answer
Hide answer
No. In tests, assert is how pytest checks results, so the rule does not fit
any line in that folder. One per-file ignore for tests/**/*.py says that
once, and new tests need nothing. Keep # noqa for a single exemption in app
code.
Going further
A few other rules also fit tests badly. PLR2004 flags a number
written straight into a comparison, and tests compare against literal values
all the time. ARG001 flags an unused argument, and a test may ask for a
fixture only for its setup. @pytest.mark.usefixtures("name") asks for it
without an argument, so the rule can stay on.
Do not copy a long list of test ignores from another project. Check what
each rule finds in your code first. After you change the config, run
ruff check on an app file and a test file. Confirm that it still reports
the findings you want.