3.4Automated tests (pytest) & typing (type hints)
The essentials in 30 seconds
- A test is an executable assertion: def test_tva(): assert tva(100) == 120 — pytest discovers test_*.py files and runs them; test the edges, not just the nominal.
- The red-green-refactor cycle protects: test first (fails), minimum next (passes), cleanup last — every fixed bug starts with a reproducing test.
- Hints document without constraining: def tva(prix: float) -> float — readable, checked by mypy, progressive typing at the boundaries first.
Running mini-case — Nabil proves his amounts are right
Nabil, junior data analyst in Algiers, fixes a VAT rate in his sales calculation and silently breaks three reports: nobody notices for a week. He writes pytest tests (nominal, negative price raising ValueError), adopts the red-green-refactor cycle, annotates his functions (prix: float) -> float and checks with mypy. The next regression fails at the first pytest -q.
Last episode of the Nabil thread — from tidied module to isolated environment to proven code.
1. The pytest test: executable proof and cycle
An automated test automatically checks that a portion of the program does what is expected, re-runnable at any time: def test_tva(): assert tva(100) == 120. With pytest, write test_*() functions with assert in test_*.py files, run with the pytest command. Test the nominal, the edge cases (empty, zero, negative — where bugs live) and errors (pytest.raises).
The protecting cycle is red-green-refactor: write the test first (it fails: red), code the minimum (it passes: green), clean up (refactor). Each bug fix starts with a reproducing test: the bug will never return unannounced. Nabil reruns pytest -q after each rate change.
2. Type hints: documentation that checks itself
Type annotations (type hints) state expected types: def moyenne(notes: list[float]) -> float. Python ignores them at runtime — code runs without them — but they document, help the editor (autocompletion, detection) and ease maintenance. Progressive typing: boundaries first (API, shared functions), internals next.
The mypy tool checks hint consistency without running the code (static analysis), ideally in continuous integration. Nabil annotates his shared calculation functions in Algiers, tests the edges (empty list, negative amount) and aims for useful tests rather than chasing 100% coverage. Hints + tests: code that re-reads well and proves itself.
🔎 Concrete example — First test guarding the calculation
Nabil secures his VAT: nominal, edge and error.
def tva(prix_ht: float, taux: float = 0.2) -> float:
if prix_ht < 0:
raise ValueError("prix negatif")
return prix_ht * (1 + taux)
import pytest
from prix import tva
def test_tva_nominal():
assert tva(100) == 120
def test_tva_negatif():
with pytest.raises(ValueError):
tva(-5)
Reading: nominal + edge + error: the minimal trio that protects.
Lesson: rerun pytest -q after each rate change.
Section vocabulary
- Automated test
- Code automatically checking that a portion of the program works as expected, re-runnable at any time.
Simple example: Nabil's VAT tests rerun after each change.
What it is not: a manual trial forgotten after two days.
Reflex: test nominal + edges + errors.
- pytest
- Test framework: test_*() functions with assert in test_*.py, run with the pytest command.
Simple example: Nabil's pytest -q after each change.
What it is not: check prints thrown away after use.
Reflex: one test_*.py file per tested module.
- assert
- Checks that a condition is true, otherwise the test fails: assert tva(100) == 120.
Simple example: Nabil's nominal VAT equality.
What it is not: a “should work” comment.
Reflex: assert the exact expected result.
- Type hints
- Annotations of expected types (a: int) -> str: document without constraining execution.
Simple example: Nabil's (prix_ht: float, taux: float = 0.2) -> float.
What it is not: a constraint blocking execution.
Reflex: annotate shared boundaries first.
- mypy
- Tool checking hint consistency without running the code (static analysis).
Simple example: Checking Nabil's calculations in continuous integration.
What it is not: a runtime test replacing hints.
Reflex: hints + mypy + tests: proven code.
In practice — Test with pytest and annotate types
- Install pytest and write a function (e.g. tva) then its tests in test_prix.py with asserts.
- Run pytest: observe the report, trigger a deliberate failure and read the diagnosis.
- Annotate your functions: def moyenne(notes: list[float]) -> float.
- Add an edge-case test (empty list, negative price) with pytest.raises for the error.
Ready-to-say script — presenting your tests (30 s)
“Calculations covered by pytest: nominal, edges and errors via pytest.raises, red-green-refactor cycle, pytest -q after each change. Shared functions annotated with hints and checked by mypy. Any regression fails before reaching the reports.”
Say in review; show the nominal + edge + error trio.
Copy-paste template — typed function + pytest tests
def moyenne(notes: list[float]) -> float:
if not notes:
raise ValueError("liste vide")
return sum(notes) / len(notes)
import pytest
from stats import moyenne
def test_moyenne_nominale():
assert moyenne([10.0, 20.0]) == 15.0
def test_moyenne_vide():
with pytest.raises(ValueError):
moyenne([])
Independent
Three tests (nominal + edge + error) are enough to secure monthly calculations.
SME in Algiers
Nabil covers VAT and averages before sending reports to managers.
Product team
Enforce pytest -q + mypy in continuous integration for each merge.
Beginner
This week: one annotated function and two tests, including one edge case.
✓ Best practices
- Write the test first (red), then the minimum (green), then clean up.
- Test edges and errors, not just the nominal case.
- Annotate shared boundaries and check with mypy.
✗ Common pitfalls
- Testing after (or never) and discovering the regression in production.
- Testing only the nominal (edges break first).
- Chasing 100% coverage as a fetish instead of useful tests.
Key takeaways
- TESTS prove code works — pytest: test_*() + assert, pytest command.
- RED-GREEN-REFACTOR: test first, minimum after, cleanup — bug = reproducing test.
- HINTS document types WITHOUT changing execution; mypy CHECKS them.
- EDGES + errors first (empty, negative, zero): that is where it breaks.
48h challenge — apply tomorrow
Cover 1 function with 3 tests (nominal + edge + error) and annotate it with hints.
- My tests cover nominal, edge and error.
- I practise red-green-refactor on each bug.
- My shared functions are annotated and checked.
Frequently asked questions
Is writing tests not a waste of time?
No, it pays off: tests catch regressions at once, allow fearless refactoring and document expected behaviour. On a living project, time saved by not hunting bugs far exceeds writing tests.
Are type hints mandatory?
No, Python ignores them at runtime. But they improve autocompletion, error detection and readability — strongly recommended in a team or on a large project, and checkable with mypy.
What does a good minimal trio of tests check?