Mathew K Analytics

Lesson 14 · Pytest deep dive

pytest Tutorial #14: Capturing Output & Logs

On testing real Python code with pytest. capsys, capfd, and caplog: real asserting on printed and logged output. Let's get into it. capsys intercepts…

⬇ Download notebook

📓 Full notebook

Download .ipynb

pytest Deep-Dive, Video 14: Capturing Output#

  • Video fourteen of an eighteen-part series on testing real Python code with pytest.
  • capsys, capfd, and caplog: real asserting on printed and logged output.
  • Let's get into it.

Part 1: The Helper From Last Time#

import pytest, tempfile, os
def run_pytest(code_str, args=None):
    with tempfile.NamedTemporaryFile(mode='w', suffix='_test.py', delete=False) as f:
        f.write(code_str)
        path = f.name
    result = pytest.main([path, *(args or [])])
    os.unlink(path)
    return result

Part 2: capsys - Capturing print() Output#

code = '''
def greet(name):
    print(f"Hello, {name}!")

def test_greet_prints_message(capsys):
    greet('Ada')
    captured = capsys.readouterr()
    assert captured.out == 'Hello, Ada!\\n'
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpv3g_axq8_test.py::test_greet_prints_message PASSED [100%]

============================== 1 passed in 0.22s ==============================
0

Part 3: readouterr() Reads and Clears#

code = '''
def test_readouterr_clears_buffer(capsys):
    print('first batch')
    first = capsys.readouterr()
    assert first.out == 'first batch\\n'
    second = capsys.readouterr()
    assert second.out == ''
    print('second batch')
    third = capsys.readouterr()
    assert third.out == 'second batch\\n'
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmp_1apgg17_test.py::test_readouterr_clears_buffer PASSED [100%]

============================== 1 passed in 0.11s ==============================
0

Part 4: capfd - Capturing at the File-Descriptor Level#

code = '''
import os

def test_capfd_catches_raw_os_write(capfd):
    os.write(1, b'raw fd write\\n')
    captured = capfd.readouterr()
    assert 'raw fd write' in captured.out
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmppwyfsyqh_test.py::test_capfd_catches_raw_os_write PASSED [100%]

============================== 1 passed in 0.12s ==============================
0

Part 5: caplog - Capturing Log Records#

code = '''
import logging

def risky_operation(value):
    if value < 0:
        logging.warning('received a negative value: %s', value)
    return abs(value)

def test_warning_is_logged(caplog):
    result = risky_operation(-5)
    assert result == 5
    assert 'received a negative value' in caplog.text

def test_no_warning_for_valid_input(caplog):
    risky_operation(5)
    assert caplog.text == ""
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpxp2v74zo_test.py::test_warning_is_logged PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpxp2v74zo_test.py::test_no_warning_for_valid_input PASSED [100%]

============================== 2 passed in 0.11s ==============================
0

Part 6: caplog.at_level - Capturing Below the Default Threshold#

code = '''
import logging

def test_info_hidden_by_default(caplog):
    logging.info('this is quiet by default')
    assert 'this is quiet' not in caplog.text

def test_info_visible_with_at_level(caplog):
    with caplog.at_level(logging.INFO):
        logging.info('now this is visible')
    assert 'now this is visible' in caplog.text
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp5dfcvaae_test.py::test_info_hidden_by_default PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp5dfcvaae_test.py::test_info_visible_with_at_level PASSED [100%]

============================== 2 passed in 0.12s ==============================
0

Part 7: A Real Pattern - Testing a Function That Prints and Logs#

code = '''
import logging

def process_order(quantity):
    if quantity > 100:
        logging.warning('unusually large order: %s', quantity)
    print(f'Processing order for {quantity} units.')
    return quantity * 10

def test_process_order_large_quantity(capsys, caplog):
    total = process_order(150)
    assert total == 1500
    captured = capsys.readouterr()
    assert 'Processing order for 150 units' in captured.out
    assert 'unusually large order' in caplog.text

def test_process_order_normal_quantity(capsys, caplog):
    total = process_order(10)
    assert total == 100
    captured = capsys.readouterr()
    assert 'Processing order for 10 units' in captured.out
    assert caplog.text == ""
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpohws5wxm_test.py::test_process_order_large_quantity PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpohws5wxm_test.py::test_process_order_normal_quantity PASSED [100%]

============================== 2 passed in 0.13s ==============================
0

Wrap-Up: What You Learned#

  • capsys intercepts sys.stdout and sys.stderr; readouterr() returns what was printed as a named tuple.
  • Each readouterr() call only returns output written since the last call, then clears the buffer.
  • capfd captures at the OS file-descriptor level, catching raw os.write calls capsys would miss.
  • caplog captures Python logging output; by default it captures WARNING and above.
  • caplog.at_level(...), used as a with block, temporarily lowers the captured threshold to INFO or DEBUG.
  • capsys and caplog can be requested together to verify both printed and logged output in one test.
  • That wraps up capturing output. Next up: Parallel Testing With pytest-xdist.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.