Mathew K Analytics

Lesson 1 · Pytest deep dive

pytest Tutorial #1: Getting Started — Your First Python Test

Video one of an eighteen-part series on testing real Python code with pytest. Test discovery, plain assert statements, and actually running pytest. Let's…

⬇ Download notebook
pytest

📓 Full notebook

Download .ipynb

pytest Deep-Dive, Video 1: Getting Started#

  • Video one of an eighteen-part series on testing real Python code with pytest.
  • Test discovery, plain assert statements, and actually running pytest.
  • Let's get into it.

Part 1: What pytest Is and Why#

import pytest
print(pytest.__version__)
9.0.2

Part 2: A Real Helper - Running pytest Against a String of Code#

import 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 3: The First Real Test Function#

code = '''
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpsgjoh7u0_test.py::test_add PASSED [100%]

============================== 1 passed in 0.19s ==============================
0

Part 4: A Real Failing Test, and What pytest Actually Shows#

code = '''
def add(a, b):
    return a + b

def test_add_wrong():
    assert add(2, 3) == 6
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpadzslho3_test.py::test_add_wrong FAILED [100%]

================================== FAILURES ===================================
_______________________________ test_add_wrong ________________________________

    def test_add_wrong():
>       assert add(2, 3) == 6
E       assert 5 == 6
E        +  where 5 = add(2, 3)

C:\Users\makmw\AppData\Local\Temp\tmpadzslho3_test.py:6: AssertionError
=========================== short test summary info ===========================
FAILED ..\..\..\Users\makmw\AppData\Local\Temp\tmpadzslho3_test.py::test_add_wrong - assert 5 == 6
============================== 1 failed in 0.30s ==============================
1

Part 5: Multiple Test Functions in One File#

code = '''
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def test_add():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, -1) == -2

def test_subtract():
    assert subtract(10, 4) == 6

def test_subtract_wrong():
    assert subtract(10, 4) == 5
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 4 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpml5tmfpx_test.py::test_add PASSED [ 25%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpml5tmfpx_test.py::test_add_negative PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpml5tmfpx_test.py::test_subtract PASSED [ 75%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpml5tmfpx_test.py::test_subtract_wrong FAILED [100%]

================================== FAILURES ===================================
_____________________________ test_subtract_wrong _____________________________

    def test_subtract_wrong():
>       assert subtract(10, 4) == 5
E       assert 6 == 5
E        +  where 6 = subtract(10, 4)

C:\Users\makmw\AppData\Local\Temp\tmpml5tmfpx_test.py:18: AssertionError
=========================== short test summary info ===========================
FAILED ..\..\..\Users\makmw\AppData\Local\Temp\tmpml5tmfpx_test.py::test_subtract_wrong - assert 6 == 5
========================= 1 failed, 3 passed in 0.12s =========================
1

Part 6: Test Discovery Rules#

import tempfile, os, subprocess, sys
def run_pytest_named(code_str, filename, args=None):
    tmpdir = tempfile.mkdtemp()
    path = os.path.join(tmpdir, filename)
    with open(path, 'w') as f:
        f.write(code_str)
    result = pytest.main([path, *(args or [])])
    return result
helper_code = 'def test_named_ok():\n    assert 1 + 1 == 2\n'
exit_code = run_pytest_named(helper_code, 'test_naming_demo.py', ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpujearbqb\test_naming_demo.py::test_named_ok PASSED [100%]

============================== 1 passed in 0.02s ==============================
0

Part 7: A File pytest Will NOT Collect#

bad_name_code = 'def test_should_be_skipped():\n    assert False\n'
exit_code = run_pytest_named(bad_name_code, 'helpers_demo.py', ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpc4z7r9lq\helpers_demo.py::test_should_be_skipped FAILED [100%]

================================== FAILURES ===================================
___________________________ test_should_be_skipped ____________________________

    def test_should_be_skipped():
>       assert False
E       assert False

C:\Users\makmw\AppData\Local\Temp\tmpc4z7r9lq\helpers_demo.py:2: AssertionError
=========================== short test summary info ===========================
FAILED ..\..\..\Users\makmw\AppData\Local\Temp\tmpc4z7r9lq\helpers_demo.py::test_should_be_skipped - assert False
============================== 1 failed in 0.02s ==============================
1

Part 8: Useful CLI-Style Flags - -v, -x, -k#

code = '''
def test_one():
    assert True

def test_two():
    assert False

def test_three():
    assert True
'''
print('--- with -x (stop at first failure) ---')
run_pytest(code, ['-v', '--no-header', '-x'])
print('--- with -k test_three (only matching tests) ---')
run_pytest(code, ['-v', '--no-header', '-k', 'test_three'])
--- with -x (stop at first failure) ---
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp87kujut9_test.py::test_one PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp87kujut9_test.py::test_two FAILED [ 66%]

================================== FAILURES ===================================
__________________________________ test_two ___________________________________

    def test_two():
>       assert False
E       assert False

C:\Users\makmw\AppData\Local\Temp\tmp87kujut9_test.py:6: AssertionError
=========================== short test summary info ===========================
FAILED ..\..\..\Users\makmw\AppData\Local\Temp\tmp87kujut9_test.py::test_two - assert False
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!
========================= 1 failed, 1 passed in 0.11s =========================
--- with -k test_three (only matching tests) ---
============================= test session starts =============================
collecting ... collected 3 items / 2 deselected / 1 selected

..\..\..\Users\makmw\AppData\Local\Temp\tmpuivvv5rw_test.py::test_three PASSED [100%]

======================= 1 passed, 2 deselected in 0.11s =======================
<ExitCode.OK: 0>

Part 9: A Real Pattern - Testing a Small Utility Module#

module_code = '''
def slugify(text):
    return text.strip().lower().replace(' ', '-')

def truncate(text, length):
    if len(text) <= length:
        return text
    return text[:length].rstrip() + '...'
'''
test_code = module_code + '''
def test_slugify_basic():
    assert slugify('Hello World') == 'hello-world'

def test_slugify_strips_whitespace():
    assert slugify('  Padded Text  ') == 'padded-text'

def test_truncate_short_text_unchanged():
    assert truncate('short', 10) == 'short'

def test_truncate_long_text_gets_ellipsis():
    result = truncate('this is a long sentence', 10)
    assert result.endswith('...')
    assert len(result) == 12
'''
exit_code = run_pytest(test_code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 4 items

..\..\..\Users\makmw\AppData\Local\Temp\tmptlkumani_test.py::test_slugify_basic PASSED [ 25%]
..\..\..\Users\makmw\AppData\Local\Temp\tmptlkumani_test.py::test_slugify_strips_whitespace PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmptlkumani_test.py::test_truncate_short_text_unchanged PASSED [ 75%]
..\..\..\Users\makmw\AppData\Local\Temp\tmptlkumani_test.py::test_truncate_long_text_gets_ellipsis PASSED [100%]

============================== 4 passed in 0.13s ==============================
0

Wrap-Up: What You Learned#

  • pytest tests are plain functions named test_*, with no special base class required.
  • Plain assert statements work directly; pytest rewrites them to show detailed left/right failure values.
  • A test file can hold as many test_ functions as needed, each reported pass or fail independently.
  • Discovery relies on naming: files test_.py or test.py, functions test, classes Test without init.
  • Passing an explicit path collects that file directly, bypassing the naming-based discovery rules.
  • -v gives verbose per-test output, -x stops at the first failure, -k filters tests by name.
  • pytest.main() returns an exit code: 0 means every test passed, non-zero means at least one failed.
  • That wraps up getting started. Next up: Fixtures Basics.

Found this useful?

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