Setting Up pytest
We install and configure pytest, then convert Session 31's manual assert-based checks into real, discoverable, well-organized pytest tests.
A real tool for running and reporting on those checks.
1. Learning Objective
By the end of this session you will be able to:
- Install pytest and understand its file/function naming conventions for test discovery
- Write a test function pytest can automatically discover and run
- Run the test suite from the command line and read its output
- Compare a failing pytest assertion's output to a bare assert's output
- Organize tests into a dedicated tests/ directory, separate from application code
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
For pytest to automatically discover a test function without any extra configuration, what must be true about its name and the file it lives in?
test_*.py or *_test.py, and within them, functions starting with test_. Following this convention means you never need to manually register a test.How do you actually run the discovered tests from the command line?
pytest in the project directory automatically discovers and runs every test file/function matching its naming convention โ no manual listing required.When a plain assert k.population == 55000000 fails inside a pytest test, what does pytest's output show you that a bare Python assert (Session 31) does not?
assert and shows you the actual runtime values involved, giving a much clearer picture of what went wrong than a bare assert's generic AssertionError.Why organize tests into a separate tests/ directory instead of mixing test files in with application code?
What is the pytest equivalent of Session 31's manual test_grow_population_rejects_negative(), which checked that calling grow_population(-5) raised ValueError?
pytest.raises(ExceptionType) is a context manager specifically for asserting that a block of code raises a given exception โ replacing Session 31's manual try/except/assert False pattern with something more concise and pytest-native.3. The Concept โ pytest Fundamentals
A bare assert just says "failed." pytest shows you the actual values on both sides, automatically.
Installing pytest
pytest is installed like any third-party package, typically with pip.
# In your terminal (not in a .py file):
# pip install pytest
Test discovery conventions
pytest automatically finds and runs tests that follow its naming conventions โ no manual registration required, unlike Session 31's hand-called test functions.
# tests/test_country.py โ file name starts with test_
from country import Country # importing the real application module
def test_summary_format(): # function name starts with test_
k = Country(name="Kenya", region="Africa", population=54000000)
assert k.summary() == "Kenya (Africa): pop. 54,000,000"
Running the suite
A single command runs every discovered test and reports a summary โ pass/fail counts and details on any failures.
# In your terminal:
# pytest
#
# Example output:
# ===== test session starts =====
# collected 3 items
#
# tests/test_country.py ... [100%]
#
# ===== 3 passed in 0.02s =====
Readable failure output
When a plain assert fails inside a pytest test, pytest shows you the actual values on both sides โ far more informative than a bare AssertionError.
def test_grow_population_increases_value():
k = Country(name="Kenya", region="Africa", population=54000000)
k.grow_population(1000000)
assert k.population == 56000000 # deliberately wrong, to see pytest's failure output
# pytest shows something like:
# assert 55000000 == 56000000
# โ pytest introspected the actual values automatically, no manual message needed
Testing for raised exceptions with pytest.raises
pytest provides a dedicated, cleaner way to assert that code raises a specific exception, replacing Session 31's manual try/except/assert False.
import pytest
def test_grow_population_rejects_negative():
k = Country(name="Kenya", region="Africa", population=54000000)
with pytest.raises(ValueError):
k.grow_population(-5)
# if grow_population does NOT raise ValueError, this test fails automatically
4. Lab
What you will build
A file called country.py (the real module) and tests/test_country.py.
Step-by-step instructions
Create country.py as a real, importable module
# country.py
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
def summary(self):
return f"{self.name} ({self.region}): pop. {self.population:,}"
def set_population(self, value):
if value < 0:
raise ValueError(f"population must be non-negative, got {value}")
self.population = value
def grow_population(self, amount):
if amount < 0:
raise ValueError(f"amount must be non-negative, got {amount}")
self.population += amount
Create the tests/ directory and test_country.py
# tests/test_country.py
import pytest
from country import Country
def test_summary_format():
k = Country(name="Kenya", region="Africa", population=54000000)
assert k.summary() == "Kenya (Africa): pop. 54,000,000"
def test_grow_population_increases_value():
k = Country(name="Kenya", region="Africa", population=54000000)
k.grow_population(1000000)
assert k.population == 55000000
Add exception tests using pytest.raises
def test_grow_population_rejects_negative():
k = Country(name="Kenya", region="Africa", population=54000000)
with pytest.raises(ValueError):
k.grow_population(-5)
def test_set_population_rejects_negative():
k = Country(name="Kenya", region="Africa", population=54000000)
with pytest.raises(ValueError):
k.set_population(-1)
Run pytest from the project root and confirm all tests pass
Run the pytest command and read the summary output.
# pytest
Deliberately break one test to see pytest's failure output, then fix it
Change an expected value to something wrong, run pytest again, read the detailed failure output, then revert it.
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
country.py |
Created | The real, importable Country module (no longer duplicated inline in a lab script). |
tests/test_country.py |
Created | A proper pytest test suite for Country, organized in a dedicated tests/ directory. |
docs/sessions/session-32/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add country.py tests/test_country.py docs/sessions/session-32/index.html
git commit -m "session-32: set up pytest with a real tests/ directory"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
For pytest to automatically discover a test function without any extra configuration, what must be true about its name and the file it lives in?
test_*.py or *_test.py, and within them, functions starting with test_. Following this convention means you never need to manually register a test.How do you actually run the discovered tests from the command line?
pytest in the project directory automatically discovers and runs every test file/function matching its naming convention โ no manual listing required.When a plain assert k.population == 55000000 fails inside a pytest test, what does pytest's output show you that a bare Python assert (Session 31) does not?
assert and shows you the actual runtime values involved, giving a much clearer picture of what went wrong than a bare assert's generic AssertionError.Why organize tests into a separate tests/ directory instead of mixing test files in with application code?
What is the pytest equivalent of Session 31's manual test_grow_population_rejects_negative(), which checked that calling grow_population(-5) raised ValueError?
pytest.raises(ExceptionType) is a context manager specifically for asserting that a block of code raises a given exception โ replacing Session 31's manual try/except/assert False pattern with something more concise and pytest-native.9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Compare the effort of writing test_grow_population_rejects_negative() with pytest.raises() versus Session 31's manual try/except/assert False version. Which communicates intent more clearly to a reader?
- Why does pytest need file and function names to follow a convention, instead of you manually telling it which functions are tests?
- What did the deliberately-broken test's failure output show you that made it easy to identify the problem?
- How does organizing tests/ separately from country.py reflect the same reasoning as splitting country_data.py from explorer.py back in Session 10?
10. What Breaks If This Knowledge Is Missing?
- Forgetting to run tests: Even a good test suite is useless if no one remembers to run it before shipping a change โ this is a process discipline this session sets up the tooling for, but does not solve by itself (CI automation solves it, but is out of scope for this curriculum).
- Testing props/output specifically (Session 33): This session set up the tooling; the next two sessions dive into what specifically to assert on โ return values and object state โ building real test coverage across the whole project.
- Confident refactoring (Layer 6): A real, runnable pytest suite is the safety net that makes Layer 6's architecture refactoring sessions safe to do boldly instead of nervously.
11. What We Learned
Python concept mastered: pytest fundamentals โ installation, test discovery conventions, running the suite, readable failure output, and pytest.raises for exception testing.
Unlocks: The project now has a real, professional testing setup โ the foundation for every remaining test-writing session in Layer 5.
Next session: Session 33 โ Testing Functions and Return Values. We dig into what specifically to assert on for functions and methods that return values, covering edge cases systematically.