Pytest Print or Dump Variables to Console for Debugging

Trying to print(var) in your pytest tests and wondering why you cannot see the output in your terminal’s standard output? This is because by default pytest captures all output sent to stdout and stderr, i.e., it intercepts the data going into the low-level file descriptors 1 and 2.

This is a good idea because pytest can now control the test run output to print a clean and understandable test summary but at the same time it becomes challenging for us when we are trying to debug our test code with print() (or similar) statements.

How do we solve this? There are a couple of options.

-s or –capture=no option

The easiest way is to run your test with the -s or --capture=no option. Say you have this piece of code:

def test_foo():
    print('Hello World')
    assert True

Then run pytest with the following options:

$ pytest -s tests/ # or pytest --capture=no tests/
========== test session starts ==========
platform linux -- ...
rootdir: ...
collected 1 item

tests/test_foo.py Hello World
.

========== 1 passed in 0.01s ==========

The Hello World that you see in the output above is because of print('Hello World') from inside the test_foo test function.

Make a Test Fail

With pytest, if a test or a setup method fails, then all the output to stdout and stderr that it captures by default, will be shown in the console with the failure traceback. This is something we could make use of easily. For instance, if you simply fail your test with an assert False like the following:

def test_foo():
    dict = {'hello': 'world'}
    print(dict)
    assert False

On running the test, you’ll get this output:

$ pytest tests/
========== test session starts ==========
platform linux -- ...
rootdir: ...
collected 1 item

testing/test_foo.py F                                                                                                                                           [100%]

========== FAILURES ==========
__________ test_foo __________

    def test_foo():
        dict = {'hello': 'world'}
        print(dict)
>       assert False
E       assert False

testing/test_foo.py:4: AssertionError
---------- Captured stdout call ----------
{'hello': 'world'}
========== short test summary info ==========
FAILED testing/test_foo.py::test_foo - assert False
========== 1 failed in 0.02s ==========

Notice the Captured stdout call section above.

-rP option

Making a passing test fail just to look at the captured output can feel like a hassle. There’s an -r option that we can pass to pytest to show extra test summary info. This extra info is configurable by passing a bunch of characters to the option. Basically, if you hit pytest --help then you’re going to see this section:

-r chars              show extra test summary info as specified by chars: (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. (w)arnings are enabled by default (see --disable-warnings), 'N' can be used to reset the list. (default: 'fE').

If you notice, P is what we’re looking for. It adds an extra section called PASSES with those tests that passed but had captured outputs. If you want to see all the passed tests regardless of captured output then the small p will do the job.

$ pytest -rP tests/
========== test session starts ==========
platform linux -- ...
rootdir: ...
collected 1 item

testing/test_foo.py .                                                                                                                                           [100%]

========== PASSES ==========
_________ test_foo __________
---------- Captured stdout call ----------
Hello World
========== 1 passed in 0.01s ==========

Notice the Captured stdout call section above. The option usage is like -rP and if you want to use p as well then -rpP.

capsys or capfd

pytest provides certain built-in fixtures that allows access to the captured stdout and stderr output only during a test execution. These fixtures are capsys, capsysbinary, capfd and capfdbinary. Instead of explaining them myself, I think pytest --fixtures does a much better job:

capsys
    Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
    ...

capsysbinary
    Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
    ...

capfd
    Enable text capturing of writes to file descriptors ``1`` and ``2``.
    ...

capfdbinary
    Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
    ...

Each fixture has a readouterr() method that returns the captured output in a namedtuple ((out, err)) which are either text (capsys and capfd) or bytes (capsysbinary and capfdbinary) objects.

You still can’t print the captured output from these fixtures but you can write them out to a file. Let’s see an example:

def test_foo(capsys):
    print("Hello World")
    out, err = capsys.readouterr()
    open('out.txt', 'w').write(out)
    open('err.txt', 'w').write(err)
    assert True

As you can see, we first specify the capsys built-in fixture in the function parameter list, then use the readouterr() method to get the captured output which in this case is Hello World. The relevant stdout output is stored in out and if there were any stderr output that would go in err.

We then write any captured output or error into separate files that we can inspect after the test has run.

We can have multiple capsys.readouterr() calls interleaved amongst multiple prints. Every time it is called, it will return the output captured till that point and the next call will capture output since the previous one only. So basically every call will not give the entire captured output of the function.

Leave a Reply

Your email address will not be published.