A Practical pytest Tutorial That Actually Fixes Early Gotchas

Table of Contents
pytest fixture
pytest fixture
pytest parametrize
pytest parametrize
pytest.raises
pytest.raises
test discovery
test discovery
Practical Pytest: Stop Guessing, Start Testing
I spent way too long wondering why my tests were "passing" when my code was definitely broken. Pytest is incredibly powerful, but it quietly lets you get away with bad habits early on.
The Golden Rule
Tests should fail when the code is broken. If you write a test and you haven't seen it fail on purpose at least once, don't trust it.
1. The Core Workflow
Install and Run
Install with pip install pytest. Run it by typing pytest in your project root. It will automatically find files named test_*.py.
Write a Basic Test
Pytest doesn't require a heavy DSL. Just write functions starting with test_ and use plain Python assert statements.
def test_addition():
assert 1 + 1 == 2
Target Specific Tests
Speed up your feedback loop by running only the specific test you are working on:
pytest test_math.py::test_addition
2. Powerful Pytest Features
Fixtures: Shared Setup
Use fixtures to set up database connections or dummy data once, instead of copy-pasting it into every test.
@pytest.fixture
def empty_cart():
return ShoppingCart()
def test_add_item(empty_cart):
empty_cart.add("apple")
assert len(empty_cart) == 1
| Fixture Scope | When it Runs | Use Case |
|---|---|---|
| function (default) | Before each test function | Isolated test data |
| class | Once per test class | Shared setup for class tests |
| module | Once per test file | Expensive DB connections |
| session | Once per test session | Global config, expensive fixtures |
Parametrize: Test Multiple Cases
Stop writing test_add_positive, test_add_negative. Use parametrization to run the same logic with different inputs.
@pytest.mark.parametrize("a,b,expected", [(1,2,3), (-1,1,0)])
def test_add(a, b, expected):
assert add(a, b) == expected
Testing Exceptions
Don't use try/except to test for errors. Use pytest.raises to guarantee an exception is thrown.
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
3. Common Patterns
Use tmp_path fixture (built-in) for isolated test files:
def test_write_read(tmp_path):
file = tmp_path / "data.txt"
file.write_text("hello")
assert file.read_text() == "hello"
Built-in fixture for patching without external library:
def test_api_call(monkeypatch):
monkeypatch.setattr("myapp.api.get", lambda: {"status": "ok"})
result = myapp.api.get()
assert result["status"] == "ok"
Mark tests and run subsets:
# In pytest.ini or pyproject.toml
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests"
]
# In test file
@pytest.mark.slow
def test_heavy_computation():
...
# Run only slow tests
pytest -m slow
# Exclude slow tests
pytest -m "not slow"
Decision Reference
| Goal | Use | Example |
|---|---|---|
| Shared test data | <code>@pytest.fixture</code> | DB connections, config objects |
| Multiple inputs, same logic | <code>@pytest.mark.parametrize</code> | Edge cases, boundary values |
| Assert exception raised | <code>pytest.raises(Error)</code> | Division by zero, invalid input |
| Isolate filesystem operations | <code>tmp_path</code> fixture | File read/write tests |
| Patch external dependency | <code>monkeypatch</code> fixture | API calls, environment vars |
| Run subset of tests | <code>@pytest.mark.slow</code> + <code>pytest -m slow</code> | CI separation, dev feedback |
You Might Also Like
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Advanced Pytest Fixtures and Parameterization Patterns
Design clean test suites with pytest fixture scoping, indirect parameterization, dynamic generator hooks, and resilient cleanup lifecycle patterns.
Read more
The Python Fundamentals You Actually Need to Start
Master foundational Python paradigms: memory references, mutable vs immutable types, scoping rules, generators, and clean object-oriented patterns.
Read more
Python's Underscores: Conventions, Not Access Modifiers
Python uses underscores as naming conventions, not enforced access modifiers—understanding the difference prevents confusing code and helps you write better APIs.
Read more