Learning Goals
3 min- Install pytest and run it with one command.
- Write tests as plain functions using plain
assert. - Read pytest's rich failure output.
- See side-by-side why pytest beats unittest on ergonomics.
Warm-Up · The Same Test, Two Ways
5 minpip install pytest
# unittest — needs a class and self.assertEqual import unittest class TestAdd(unittest.TestCase): def test_add(self): self.assertEqual(add(2, 3), 5) # pytest — just a function and a plain assert def test_add(): assert add(2, 3) == 5
pytest lets you write tests as ordinary functions with ordinary assert — no class, no special methods. And when an assert fails, pytest rewrites it to show you both sides automatically. Less code, better failures. That's why it dominates.
New Concept · Plain Functions, Plain Asserts
14 minA pytest test file
# test_mathtools.py from mathtools import is_prime, factorial def test_is_prime(): assert is_prime(17) assert not is_prime(4) def test_factorial(): assert factorial(5) == 120 def test_factorial_negative(): import pytest with pytest.raises(ValueError): factorial(-1)
Run it
$ pytest ========================= test session starts ========================= collected 3 items test_mathtools.py ... [100%] ========================== 3 passed in 0.01s ==========================
The killer feature: assert rewriting
def test_factorial(): assert factorial(5) == 119 # wrong on purpose
def test_factorial(): > assert factorial(5) == 119 E assert 120 == 119 E + where 120 = factorial(5) test_mathtools.py:7: AssertionError
From a plain assert, pytest shows the actual value (120), the expected (119), AND the call that produced it — without you writing any message. unittest needs assertEqual to get half of that.
Common commands
pytest run all test_*.py in the tree pytest test_bank.py one file pytest -v verbose: one line per test pytest -k "discount" only tests matching a keyword pytest -x stop at first failure pytest -q quiet pytest test_bank.py::test_overdraw one specific test
It runs unittest too
pytest discovers and runs your existing unittest.TestCase classes as well — so you can adopt it gradually without rewriting everything.
Worked Example · Port the Bank Tests
12 minHere's the Lesson 7 bank suite rewritten in pytest — notice how much shrinks:
# test_bank.py (pytest version) import pytest from bank import BankAccount def test_starts_at_zero(): assert BankAccount().balance == 0 def test_deposit_increases_balance(): acc = BankAccount() acc.deposit(100) assert acc.balance == 100 def test_withdraw_decreases_balance(): acc = BankAccount(100) acc.withdraw(40) assert acc.balance == 60 def test_overdraw_raises(): with pytest.raises(ValueError): BankAccount(50).withdraw(100) def test_negative_deposit_raises(): with pytest.raises(ValueError, match="positive"): BankAccount().deposit(-5)
$ pytest test_bank.py -v test_bank.py::test_starts_at_zero PASSED test_bank.py::test_deposit_increases_balance PASSED test_bank.py::test_withdraw_decreases_balance PASSED test_bank.py::test_overdraw_raises PASSED test_bank.py::test_negative_deposit_raises PASSED 5 passed in 0.01s
Read the diff
No class, no self, no unittest.main(). pytest.raises(ValueError, match="positive") checks both the exception AND its message in one line. The whole file is shorter and reads like plain Python. This is why nearly every modern Python project uses pytest.
Try It Yourself
13 minWrite 4 plain-function tests for any module. Run pytest -v. Confirm the green output.
Make an assert fail on a complex expression (e.g. assert sorted(x) == [1,2,3]). Study how pytest expands the comparison in the failure output.
Take a TestCase from earlier and rewrite it as pytest functions. Count the lines saved. Use pytest.raises for the error cases.
Mini-Challenge · Side-by-Side
8 minPick one class and write its test suite TWICE — once in unittest, once in pytest. Put the two files beside each other. Write 3 bullet points comparing them (lines of code, readability, failure messages).
Recap
3 minpytest: plain functions, plain assert, no class needed. It rewrites assertions to show actual vs expected automatically, and pytest.raises(Exc, match=...) checks exceptions + messages in one line. It even runs your unittest classes, so adoption is gradual. Run with pytest [-v] [-k] [-x]. From here on, we use pytest. Next: the discovery rules.
Vocabulary Card
- pytest
- The popular third-party test framework: functions + plain asserts.
- assert rewriting
- pytest's magic that expands a failed plain assert into a detailed message.
- pytest.raises
- Context manager asserting a block raises an exception (with optional message match).
- collected items
- The tests pytest discovered to run.
Homework
4 minInstall pytest. Convert a full earlier test suite (≥ 8 tests) to pytest functions, using pytest.raises for all error paths and at least one match= message check. Get it green with pytest -v, then break one test to admire the failure output.
Model on test_bank.py (pytest version). Plain functions, plain asserts, pytest.raises for errors.