pytest is the standard Python test tool. Tests are plain functions whose names start with test_, in files named test_*.py, using ordinary assert statements. Run them all with pytest.
pytest.raises checks that code raises the right exception. @pytest.mark.parametrize runs one test with many inputs — perfect for boundaries.
Fixtures provide ready-made test data or resources. pytest's built-in tmp_path fixture gives each test its own temporary folder.
def grade_for(score: float) -> str:
if not 0 <= score <= 100:
raise ValueError("score must be between 0 and 100")
if score >= 75:
return "A"
if score >= 65:
return "B"
if score >= 45:
return "C"
if score >= 30:
return "D"
return "F"import pytest
from grading import grade_for
@pytest.mark.parametrize(
("score", "expected"),
[(100, "A"), (75, "A"), (74, "B"), (65, "B"), (45, "C"), (30, "D"), (29, "F"), (0, "F")],
)
def test_grade_boundaries(score: float, expected: str) -> None:
assert grade_for(score) == expected
@pytest.mark.parametrize("bad", [-1, 101])
def test_rejects_out_of_range(bad: float) -> None:
with pytest.raises(ValueError, match="between 0 and 100"):
grade_for(bad)
def test_report_file(tmp_path) -> None:
report = tmp_path / "report.txt"
report.write_text(grade_for(88), encoding="utf-8")
assert report.read_text(encoding="utf-8") == "A"pip install pytest
pytest -qKey points
- Plain
assert+ functions namedtest_*— no boilerplate. - Parametrize boundary values instead of copy-pasting tests.
- Use
tmp_pathfor file tests so nothing touches real data.
Exercise
Write pytest tests for your parse_line function from the Exceptions lesson: valid lines, each kind of invalid line (using pytest.raises), and extra whitespace around values.
Show solution
Try the exercise yourself first — then compare your approach with this one.
One parametrized test covers the valid lines (including extra spaces), and another uses pytest.raises(..., match=...) to check that each kind of invalid line fails with the right message.
class InvalidLineError(Exception):
pass
def parse_line(line: str) -> tuple[str, int]:
if "," not in line:
raise InvalidLineError("missing comma")
name, _, score_text = line.partition(",")
name = name.strip()
if not name:
raise InvalidLineError("empty name")
try:
score = int(score_text)
except ValueError as err:
raise InvalidLineError("score is not a whole number") from err
if not 0 <= score <= 100:
raise InvalidLineError("score out of range")
return name, scoreimport pytest
from parsing import InvalidLineError, parse_line
@pytest.mark.parametrize(
("line", "expected"),
[
("Amina,88", ("Amina", 88)),
(" Juma , 42 ", ("Juma", 42)),
("Neema,0", ("Neema", 0)),
("Ali,100", ("Ali", 100)),
],
)
def test_valid_lines(line: str, expected: tuple[str, int]) -> None:
assert parse_line(line) == expected
@pytest.mark.parametrize(
("line", "message"),
[
("Amina 88", "missing comma"),
(" ,70", "empty name"),
("Neema,abc", "not a whole number"),
("Ali,101", "out of range"),
("Ali,-1", "out of range"),
],
)
def test_invalid_lines(line: str, message: str) -> None:
with pytest.raises(InvalidLineError, match=message):
parse_line(line)