Pytest’s logging capture is a powerful tool that lets you treat log messages as first-class citizens in your tests, enabling you to verify not just return values but also the side effects of your code’s execution.

Let’s see it in action. Imagine you have a function that logs some information:

import logging

logger = logging.getLogger(__name__)

def process_data(data):
    if not data:
        logger.warning("Received empty data, processing will be skipped.")
        return False
    logger.info(f"Processing data: {data}")
    # ... actual processing ...
    logger.debug("Data processing complete.")
    return True

Here’s how you’d test this function using pytest’s caplog fixture to capture and assert on those log messages:

import pytest
import logging
from your_module import process_data # Assuming process_data is in your_module.py

def test_process_data_with_empty_input(caplog):
    # Set the logging level for the root logger to DEBUG to ensure all messages are captured
    caplog.set_level(logging.DEBUG)

    result = process_data([])

    assert result is False
    # Assert that a specific warning message was logged
    assert "Received empty data, processing will be skipped." in caplog.text
    # Assert that a specific info message was NOT logged
    assert "Processing data:" not in caplog.text

def test_process_data_with_valid_input(caplog):
    caplog.set_level(logging.DEBUG)
    data_to_process = {"key": "value"}
    result = process_data(data_to_valid_input)

    assert result is True
    # Assert that an info message containing the data was logged
    assert "Processing data: {'key': 'value'}" in caplog.text
    # Assert that a debug message was logged
    assert "Data processing complete." in caplog.text

In these examples, caplog is a pytest fixture that intercepts log messages. By default, it captures messages from the root logger and any loggers that inherit from it. caplog.set_level(logging.DEBUG) is crucial; it tells the capturing mechanism to pay attention to messages of severity DEBUG and above. Without this, if your code logs at DEBUG level and your root logger is set to INFO, you won’t capture those DEBUG messages.

The caplog.text attribute provides the captured logs as a single string, making it easy to check for the presence or absence of specific messages using standard string assertions. You can also iterate through caplog.records for more granular inspection of each log entry, allowing you to check the level, logger name, and message content individually.

This approach allows you to test the behavior of your logging system, ensuring that critical information is being recorded, that the correct severity levels are used, and that sensitive data isn’t accidentally logged. It’s particularly useful for debugging, as you can directly assert that the diagnostic messages you expect to see during an error condition are indeed present.

The real power of caplog lies in its ability to filter and inspect individual LogRecord objects. When you iterate over caplog.records, each item is a standard Python logging.LogRecord instance. This means you can perform assertions not just on the message string itself, but also on its levelname, name (the logger’s name), pathname, lineno, and even custom exc_info if exceptions are being logged. For instance, you could write a test like:

def test_specific_logger_warning(caplog):
    caplog.set_level(logging.WARNING)
    # Assume some code here triggers a warning from 'my_app.utils'
    # ...
    found_warning = False
    for record in caplog.records:
        if record.levelname == "WARNING" and record.name == "my_app.utils":
            assert "Specific error condition occurred" in record.message
            found_warning = True
            break
    assert found_warning, "Expected warning from my_app.utils not found."

This level of control allows for highly specific assertions, ensuring that not only is a message logged, but it’s logged by the correct component and at the correct severity, which is essential for robust application monitoring and debugging.

Beyond simply checking for message content, caplog also allows you to temporarily override the logging configuration for the duration of a test. This is incredibly useful if your application has complex logging handlers or formatters that you don’t want to interfere with your tests, or if you need to ensure a specific handler is active for a test. You can use caplog.handler to access the handler pytest adds and caplog.formatter to access its formatter.

The next step after mastering log capture is understanding how to integrate pytest with more advanced logging scenarios, such as structured logging (e.g., JSON logging) and how to test the behavior of custom logging handlers.

Want structured learning?

Take the full Pytest course →