Pytest warnings are a surprisingly effective way to catch subtle bugs and enforce API stability, but they can also become a noisy distraction if not managed properly.

Let’s see how pytest handles warnings with a simple example.

# test_warnings.py
import pytest

def function_that_warns():
    warnings.warn("This is a deprecation warning", DeprecationWarning)
    return 1

def test_function_warning():
    assert function_that_warns() == 1

If you run pytest on this file, you’ll see the DeprecationWarning printed in your terminal output, likely after the test passes. Pytest, by default, shows warnings that are not filtered out.

The core problem pytest solves with warnings is to make them first-class citizens in your test suite. Instead of just being something that prints to stderr and might get lost, you can:

  1. Capture and display them: See what warnings your code is generating.
  2. Fail tests on specific warnings: Treat a warning as a test failure if it’s something you want to address immediately.
  3. Assert specific warning messages: Verify that a particular warning with a specific message is raised.

Let’s dive into how you control this.

Filtering Warnings: The filterwarnings Mark

The most common way to manage warnings is by using the filterwarnings mark. This mark allows you to apply standard Python warning filter rules directly to your tests. The syntax mirrors the -W command-line option for Python.

# test_warnings.py
import pytest
import warnings

def deprecated_function():
    warnings.warn("This function is deprecated, use new_function instead.", DeprecationWarning)
    return "old value"

def another_deprecated_function():
    warnings.warn("This is a different warning.", UserWarning)
    return "another old value"

@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_ignore_deprecation():
    assert deprecated_function() == "old value"

def test_show_user_warning():
    assert another_deprecated_function() == "another old value"

Running pytest on this would show the UserWarning from another_deprecated_function but hide the DeprecationWarning from deprecated_function because of the @pytest.mark.filterwarnings("ignore::DeprecationWarning") line.

The filter syntax is [action:]message:category:module:lineno.

  • action: ignore, always, default, module, once, error.
  • message: A regex string matching the warning message.
  • category: The warning class (e.g., DeprecationWarning, UserWarning).
  • module: A regex string matching the module name.
  • lineno: An integer matching the line number.

You can apply multiple filters by providing a list to the mark:

@pytest.mark.filterwarnings(
    "ignore::DeprecationWarning",
    "error::UserWarning:my_module.*"
)
def test_multiple_filters():
    # ...

Asserting Warnings: The pytest.warns Context Manager

Sometimes, you don’t just want to see a warning; you want to guarantee it’s raised. This is where pytest.warns comes in. It acts as a context manager that asserts a warning of a specific type and message is issued within its block.

# test_warnings.py
import pytest
import warnings

def function_raising_specific_warning():
    warnings.warn("Specific configuration option missing.", UserWarning)
    return True

def function_raising_other_warning():
    warnings.warn("Something else happened.", RuntimeWarning)
    return False

def test_assert_specific_warning():
    with pytest.warns(UserWarning, match="Specific configuration option missing."):
        assert function_raising_specific_warning() is True

def test_assert_no_warning_raised():
    with pytest.warns(UserWarning):
        # This will fail because no UserWarning is raised
        assert function_raising_other_warning() is False

def test_assert_wrong_warning_type():
    with pytest.warns(DeprecationWarning, match="Specific configuration option missing."):
        # This will fail because RuntimeWarning is raised, not DeprecationWarning
        assert function_raising_other_warning() is False

The match argument uses a regex to check the warning message. If match is omitted, any warning of the specified category will pass the assertion. If the block completes without the expected warning being raised, or if a different warning is raised (and not explicitly ignored), the test will fail.

Controlling Default Warning Behavior: pytest.ini

For more global control, you can configure warning filters in your pytest.ini file. This is especially useful for project-wide settings.

# pytest.ini
[pytest]
filterwarnings =
    ignore::DeprecationWarning:my_package.*
    error::UserWarning:my_module.sub_module

This configuration will apply these filters to all tests in your project unless overridden by marks in individual test files.

The One Thing Most People Don’t Know: warnings.catch_warnings vs. pytest.warns

While pytest.warns is convenient, it’s important to understand its relationship with Python’s built-in warnings module. Pytest’s warns context manager internally uses warnings.catch_warnings to temporarily modify the warning filter state. This means that any warnings issued within the pytest.warns block are captured by pytest. However, if you manually use warnings.catch_warnings in your test code outside of a pytest.warns block, pytest might not be able to capture those warnings for assertion unless you configure it to do so via pytest.ini or command-line options. The primary benefit of pytest.warns is its integration with pytest’s assertion introspection and reporting.

The Next Problem You’ll Hit

Once you’ve mastered filtering and asserting warnings, you’ll likely encounter the challenge of ensuring your tests themselves don’t accidentally introduce new warnings that get ignored or missed.

Want structured learning?

Take the full Pytest course →