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…
- CoursePytest deep dive
- Lesson14 of 18
- FormatJupyter notebook · 7 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbpytest 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)
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)
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)
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)
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)
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)
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.



