Lesson 5 · Pytest deep dive
pytest Tutorial #5: Markers — Organising and Selecting Tests
On testing real Python code with pytest. skip, skipif, xfail, and defining custom markers. Let's get into it. @pytest.mark.skip unconditionally skips a…
- CoursePytest deep dive
- Lesson5 of 18
- FormatJupyter notebook · 7 code cells
What you'll learn
- The Helper From Last Time
- @pytest.mark.skip - Unconditionally Skipping a Test
- @pytest.mark.skipif - Conditional Skipping
- @pytest.mark.xfail - Expected Failure
- xfail(strict=True) - Catching an Unexpected Pass
- Custom Markers and -m Filtering
- A Real Pattern - Platform-Sensitive Tests
- Wrap-Up: What You Learned
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbpytest Deep-Dive, Video 5: Markers#
- Video five of an eighteen-part series on testing real Python code with pytest.
- skip, skipif, xfail, and defining custom markers.
- 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: @pytest.mark.skip - Unconditionally Skipping a Test#
code = '''
import pytest
@pytest.mark.skip(reason='not implemented yet')
def test_future_feature():
assert False
def test_normal():
assert True
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 3: @pytest.mark.skipif - Conditional Skipping#
code = '''
import sys
import pytest
@pytest.mark.skipif(sys.version_info < (3, 8), reason='requires Python 3.8+')
def test_needs_modern_python():
assert sys.version_info >= (3, 8)
@pytest.mark.skipif(sys.version_info >= (4, 0), reason='not relevant on Python 4')
def test_still_python_three():
assert sys.version_info.major == 3
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 4: @pytest.mark.xfail - Expected Failure#
code = '''
import pytest
def buggy_reverse(items):
return items
@pytest.mark.xfail(reason='buggy_reverse does not actually reverse yet')
def test_buggy_reverse():
assert buggy_reverse([1, 2, 3]) == [3, 2, 1]
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 5: xfail(strict=True) - Catching an Unexpected Pass#
code = '''
import pytest
def already_fixed_reverse(items):
return list(reversed(items))
@pytest.mark.xfail(reason='thought this was still broken', strict=True)
def test_incorrectly_still_marked_xfail():
assert already_fixed_reverse([1, 2, 3]) == [3, 2, 1]
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 6: Custom Markers and -m Filtering#
code = '''
import pytest
@pytest.mark.slow
def test_slow_computation():
total = sum(range(1000))
assert total == 499500
def test_fast_check():
assert 1 + 1 == 2
'''
print('--- running everything ---')
run_pytest(code, ['-v', '--no-header', '-W', 'ignore::pytest.PytestUnknownMarkWarning'])
print('--- running only -m slow ---')
exit_code = run_pytest(code, ['-v', '--no-header', '-m', 'slow', '-W', 'ignore::pytest.PytestUnknownMarkWarning'])
print(exit_code)
Part 7: A Real Pattern - Platform-Sensitive Tests#
code = '''
import sys
import pytest
def normalize_case_for_platform(name, platform):
if platform.startswith('win') or platform == 'darwin':
return name.lower()
return name
@pytest.mark.skipif(not sys.platform.startswith('linux'), reason='linux-specific case-sensitivity check')
def test_linux_keeps_case():
assert normalize_case_for_platform('MyFile.TXT', 'linux') == 'MyFile.TXT'
@pytest.mark.xfail(reason='non-ASCII casing does not collapse to plain ASCII yet')
def test_windows_unicode_casing():
assert normalize_case_for_platform('stanbul.txt', 'win32') == 'istanbul.txt'
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Wrap-Up: What You Learned#
- @pytest.mark.skip unconditionally skips a test, reported skipped rather than passed or failed.
- @pytest.mark.skipif skips a test only when a given boolean condition evaluates true at collection time.
- @pytest.mark.xfail marks a test as expected to fail, reported XFAIL without failing the overall run.
- xfail(strict=True) turns an unexpected pass, XPASS, into a genuine failure, catching a stale marker.
- Any name after pytest.mark, like slow, becomes a custom marker; -m filters the run to matching tests.
- Custom markers trigger a warning until formally registered, filterable for a quick demo.
- skipif and xfail combine naturally to document platform differences and known, unfixed edge cases.
- That wraps up markers. Next up: Assertions Deep-Dive.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



