Python: capture stdout and stderr in unittest

I’m not quite sure what this is, but it has certainly captured my attention. (Edit: it's a composting tumbler, thanks to Eric Brubacker for identifying it!)

When testing code that outputs to the terminal through either standard out (stdout) or standard error (stderr), you might want to capture that output and make assertions on it. To do so, use contextlib.redirect_stdout() and contextlib.redirect_stderr() to redirect the respective output streams to in-memory buffers that you can then inspect and assert on.

For example, say we wanted to test this function:

def print_silly(string: str) -> None:
    # Alternate the case of each letter
    sillified = "".join(
        c.upper() if i % 2 == 0 else c.lower() for i, c in enumerate(string)
    )
    print(sillified)

We could write a test using redirect_stdout() like so:

from contextlib import redirect_stdout
from io import StringIO
from unittest import TestCase

from example import print_silly


class PrintSillyTests(TestCase):
    def test_print_silly(self):
        with redirect_stdout(StringIO()) as buffer:
            print_silly("What a lovely day!")

        self.assertEqual(buffer.getvalue(), "WhAt a lOvElY DaY!\n")

redirect_stdout() takes a file-like object to which it will redirect output, and then returns that object. That allows us to use a StringIO() as an in-memory buffer to capture the output, which we can call getvalue() on to retrieve the captured text.

redirect_stderr() works the same way, but for stderr.

Simplify capturing streams

To simplify capturing both stdout and stderr, you can copy in this extra decorator/context manager made with contextlib.contextmanager():

from contextlib import contextmanager, redirect_stderr, redirect_stdout
from io import StringIO
from typing import Generator, TextIO


@contextmanager
def capture_output() -> Generator[tuple[TextIO, TextIO]]:
    """
    Capture both stdout and stderr into StringIO buffers.

    Source: https://adamj.eu/tech/2025/08/29/python-unittest-capture-stdout-stderr/
    """
    with (
        redirect_stdout(StringIO()) as out,
        redirect_stderr(StringIO()) as err,
    ):
        yield out, err

Use it like so (here it has been placed in a testing module):

from unittest import TestCase

from example import print_silly
from testing import capture_output


class PrintSillyTests(TestCase):
    def test_print_silly(self):
        with capture_output() as (out, err):
            print_silly("What a lovely day!")

        self.assertEqual(out.getvalue(), "WhAt a lOvElY DaY!\n")
        self.assertEqual(err.getvalue(), "")

I like this pattern as it’s clear without being too long.

Assert on log messages

If the output you’re checking comes through the standard library’s logging module, rather than directly through print(), it’s better to use TestCase.assertLogs() context manager instead of redirecting stdout or stderr. For example, if we had this function that logged a message:

import logging

logger = logging.getLogger(__name__)


def log_silly(string: str) -> None:
    sillified = "".join(
        c.upper() if i % 2 == 0 else c.lower() for i, c in enumerate(string)
    )
    logger.info(sillified)

We could test it like so:

from unittest import TestCase

from example import log_silly


class LogSillyTests(TestCase):
    def test_log_silly(self):
        with self.assertLogs(level="INFO") as cm:
            log_silly("Max. My Name's Max. That's My Name.")
        self.assertEqual(
            cm.output,
            ["INFO:example:MaX. mY NaMe's mAx. ThAt's mY NaMe."],
        )

In pytest

If you use pytest to run your tests, the above pattern will work, but you may also wish to use its built-in capsys fixture. Within a unittest class, you can use it with a custom fixture in your test class that attaches the capsys fixture to the TestCase instance:

from unittest import TestCase

import pytest

from example import print_silly


class PrintSillyTests(TestCase):
    @pytest.fixture(autouse=True)
    def pytest_setup(self, capsys):
        self.capsys = capsys

    def test_print_silly(self):
        print_silly("What a lovely day!")

        captured = self.capsys.readouterr()
        assert captured.out == "WhAt a lOvElY DaY!\n"
        assert captured.err == ""

Otherwise, within regular pytest tests, in functions or plain classes, you can use the capsys fixture directly as a function argument:

from example import print_silly


def test_print_silly(capsys):
    print_silly("What a lovely day!")

    captured = capsys.readouterr()
    assert captured.out == "WhAt a lOvElY DaY!\n"
    assert captured.err == ""

Avoid the tools in test.support

If you search Python’s documentation or other sources, you may find test.support.captured_stdout() and the similar captured_stderr(). These work like wrappers around redirect_stdout() and redirect_stderr(), creating the StringIO buffers for you:

from test.support import captured_stdout

with captured_stdout() as stdout:
    print("Hello, World!")
assert stdout.getvalue() == "Hello, World!\n"

However, you should avoid using these tools in your own code. The test.support module is an internal part of Python’s own test suite, with no guarantees of stability or backwards compatibility. Additionally, it may not be available in all Python distributions or implementations—for example, it’s not available in the Python distributions from uv.

Fin

May you capture all the output that you expect,

—Adam


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,