3.4 · Automated tests (pytest) & typing (type hints)

Level 3 · Intermediate Python

3.4Automated tests (pytest) & typing (type hints)

Goal: make code reliable with automated pytest tests (nominal + edge + errors) and annotate types (hints at boundaries, mypy) — the two marks of professional code.
Estimated time: 14 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: “Tests waste time and hints are mandatory to run.” No, reversed: tests save time as soon as the project lives (regressions caught, fearless refactoring), and hints are optional at runtime. Testing after or never + chasing 100% coverage as a fetish: two dead ends. Test first, edges included, hints at boundaries.
Proven, typed code Tests first, minimum next, hints at boundaries test (red) Nominal +edges + errorscode (green) Minimumthat passeshints (boundaries) Annotate,check via mypy
Tests first, minimum next, hints at boundaries
Text version of the diagram (SEO / screen readers / translation): 1) Test: Nabil writes a nominal test_tva + negative (pytest.raises) before touching the calculation. 2) Code: he fixes the minimum to go green, then cleans up. 3) Hints: his shared functions are annotated (float -> float) and checked by mypy. Lesson: prove before and after — the bug never returns unannounced.

🔎 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

  1. Install pytest and write a function (e.g. tva) then its tests in test_prix.py with asserts.
  2. Run pytest: observe the report, trigger a deliberate failure and read the diagnosis.
  3. Annotate your functions: def moyenne(notes: list[float]) -> float.
  4. Add an edge-case test (empty list, negative price) with pytest.raises for the error.
You write pytest tests and annotate your functions: secure, documented, maintainable code.

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.

Section exit checklist
  • My tests cover nominal, edge and error.
  • I practise red-green-refactor on each bug.
  • My shared functions are annotated and checked.
Self-rating 1-5: could a bug return without failing a test?are my boundaries annotated?

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.

Check your understanding

What does a good minimal trio of tests check?

Additional resources (optional, to go further)

Level 3 examFresh, harder questions to test what you've learned