A deprecation warning is a message from the future: the thing you are calling will stop working in a version you have not upgraded to yet. Left as warnings, those messages scroll past in a summary nobody reads until the upgrade arrives and the suite breaks in forty places at once. Turned into errors, each one fails the test that triggered it on the day it first appears, when fixing it is a one-line change.
The obstacle is noise. A modern dependency tree emits warnings you cannot fix, and a naive -W error fails the suite on the first one. Teams that try it that way usually revert within a day and conclude that strict warnings are impractical, when the real problem was an all-or-nothing configuration applied to a suite that had accumulated years of tolerated messages. The approach below keeps the strictness for everything the team controls and makes each tolerated exception explicit, scoped and temporary. The workable configuration is error by default, plus a short list of narrow, documented ignores for third-party warnings — each scoped to one category and one module, each with a note on when to revisit.
Prerequisites
pytest >= 8.0.- Familiarity with Python's warning categories:
DeprecationWarning,PendingDeprecationWarning,ResourceWarning,RuntimeWarning,UserWarning. - The configuration file conventions in pyproject.toml vs pytest.ini.
Solution
# pyproject.toml
[tool.pytest.ini_options]
filterwarnings = [
# 1. Every warning is an error unless a later entry says otherwise.
"error",
# 2. Narrow ignores for third-party noise — category AND module.
# botocore: datetime.utcnow() deprecation, fixed upstream in 1.35; revisit on bump.
"ignore:datetime.datetime.utcnow:DeprecationWarning:botocore.*",
# a transitive dependency's pkg_resources import; no fix available yet.
"ignore::DeprecationWarning:pkg_resources.*",
# 3. Resource warnings from our own code are real bugs: keep them errors.
# (No entry needed — "error" already covers ResourceWarning.)
]
import pytest
from myapp.legacy import old_api
def test_old_api_warns_before_removal():
# Asserting on the warning makes it a tested behaviour, not a tolerated one.
with pytest.warns(DeprecationWarning, match="old_api is deprecated"):
old_api()
Each ignore names a category, and most name a module. That precision is the point: the botocore entry silences one message from one package, so the same deprecation raised by your own code still fails.
Why this works
pytest applies filterwarnings entries using Python's own warning-filter machinery, where each entry has the form action:message:category:module:lineno and later entries take precedence. Putting error first establishes the default; each subsequent ignore overrides it for the warnings it matches. The module field is a regular expression matched against the module the warning is attributed to, which is what allows a filter to target one dependency without silencing the category everywhere.
The filters are applied per test, around each test's execution, so a warning is converted to an exception inside the test that triggered it. The failure therefore points at the exact test and line, rather than appearing in a summary at the end of the run with no clear owner.
Edge cases and failure modes
- Ignores without a module.
ignore::DeprecationWarningsilences every deprecation, including your own. Always scope by module unless the warning genuinely comes from everywhere. - Warnings at import time. A warning raised while collecting a module happens outside any test and is reported as a collection error. Fix the import or add a module-scoped ignore.
ResourceWarningdepends on garbage collection. Unclosed files and sockets are reported when the object is collected, which may be in a later test. Run with-X tracemallocto see where the resource was allocated.- Ignores that outlive their reason. An ignore added for a bug fixed upstream two years ago hides nothing but costs nothing either — until the same message returns from your own code. Comment every ignore with the reason and a revisit condition.
-Won the command line. Command-line-Wentries are applied after the configuration and override it. Keep policy in configuration so every runner behaves the same.
Which categories deserve which treatment
Not every warning category means the same thing, and a good configuration treats them differently rather than uniformly.
DeprecationWarning and PendingDeprecationWarning are the reason to do any of this. They announce future breakage with a lead time, and treating them as errors converts that lead time into a to-do item on the day the warning first appears. From your own code they should always be errors; from dependencies they are errors unless scoped out with a documented reason.
ResourceWarning reports an unclosed file, socket or subprocess — almost always a genuine bug in the code under test, and one that leaks file descriptors in production. Keep it an error with no ignores; the fix is a with statement.
RuntimeWarning covers several unrelated things, the most important being "coroutine was never awaited", which in a test nearly always means an assertion proved nothing. That alone justifies keeping the category an error, as the patching guides in this site argue.
UserWarning is whatever a library author chose to say. Treat it case by case: some are actionable, many are informational, and the right response is to read each one once and decide.
Warnings as an upgrade early-warning system
The largest payoff of error-by-default arrives at dependency upgrades. A library that deprecates an API typically warns for one or two minor releases before removing it. A suite that errors on deprecations surfaces each one the day the warning is introduced — usually in a routine minor-version bump — when the fix is small and the context is fresh.
The contrast with the alternative is stark. A suite that tolerates warnings accumulates them silently across a dozen minor upgrades, and the major version that finally removes the deprecated APIs breaks everything at once. What would have been twelve five-minute fixes spread over a year becomes a multi-day migration under pressure, with no record of which call sites were warned about when.
Running the suite against pre-release versions of key dependencies in a scheduled job extends the early warning further still: new deprecations appear in the suite weeks before the release ships, and the ignores list becomes a forward-looking inventory of upcoming work rather than a backward-looking list of tolerated noise. That job should report rather than fail, since a pre-release is allowed to be wrong; its value is the advance list of call sites that will need attention, delivered while there is still time to address them at leisure.
Adopting it on an existing suite
Switching an established suite to error-by-default all at once usually produces dozens of failures, which is discouraging and mostly noise. A staged adoption avoids that.
First, collect the inventory. Run the suite once with warnings reported but not raised and group them by category and origin: pytest -W default -p no:randomly -q 2>&1 | grep -E "Warning" | sort | uniq -c | sort -rn. The result is usually a short list — a handful of distinct messages, each repeated many times.
Second, add error plus an explicit ignore for every current offender, each with a comment. The suite goes green immediately, and from that moment any new warning fails. That is the essential property: the list can only shrink.
Third, work through the ignores, fixing the ones in your own code and removing the corresponding entries. Third-party ones stay until the dependency is upgraded, at which point removing the entry is part of the upgrade. A reviewer checking an upgrade pull request can then confirm that the matching ignore was deleted, which keeps the list from quietly outliving its reasons.
Frequently Asked Questions
In what order are filterwarnings entries applied?
Later entries take precedence over earlier ones, matching Python's own warning filters. Put the broad rule — usually error — first, and the specific ignores after it, so each ignore overrides the blanket error for the warnings it names.
How do I ignore a warning from one third-party package only?
Match on the module with the fourth field of the filter: ignore::DeprecationWarning:somepackage.*. That silences the category only when the warning is attributed to that package, leaving the same category an error everywhere else.
How do I assert that my own code emits a warning?
Use pytest.warns(DeprecationWarning, match="...") as a context manager around the call. It fails if the warning is not emitted, and inside it the warning does not count as an error even with error-by-default configured.
Related
- pytest Configuration Best Practices — where this setting sits among the others worth enforcing.
- Patching Async Code & Coroutines — why "coroutine was never awaited" should be an error.
- Capturing Logs with caplog and log_cli — the companion capture mechanism for log records.
- pytest-asyncio in Depth — surfacing plugin deprecations before an upgrade breaks the suite.
← Back to pytest Configuration Best Practices