Skip to content
← All Python articles

Python explainer

Allow assert in Tests but Not in App Code

About 4 min to read. Allow extra time to work through examples.

Replace assert with an explicit check wherever the check must always run, and set up Ruff's S101 rule to flag assert in app code but not in tests.

This article uses Python. You can make it your practice mode to browse related content across the site.

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-select adds rules to the ones Ruff already checks. select replaces that list, so use it only when you mean to.
  • per-file-ignores turns rules off for the files that match a pattern. Change the pattern if your tests live somewhere else.
  • RUF100 flags a # noqa comment 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 assert in app code, add # noqa: S101 to 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

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.

step 19 of 19

Testing with pytest and Hypothesis

You've reached the last step

Revisit the track to review its lessons and exercises, or explore another topic.

Explore more tracks