Why pytest?

Python ships with a built-in testing module called unittest, but most modern Python developers choose pytest instead. It's simpler to write, produces cleaner output, and has a powerful plugin ecosystem. If you're writing tests in Python today, pytest is the de facto standard.

Installation

pip install pytest

Verify your installation:

pytest --version

Your First Test

Create a file named test_math.py. pytest automatically discovers files and functions that match naming conventions:

def add(a, b):
    return a + b

def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_with_zero():
    assert add(0, 10) == 10

def test_add_negative_numbers():
    assert add(-1, -1) == -2

Run your tests:

pytest test_math.py

Test Discovery Rules

  • Files must be named test_*.py or *_test.py
  • Test functions must start with test_
  • Test classes must start with Test (no __init__ needed)

Fixtures: Reusable Test Setup

Fixtures are one of pytest's most powerful features. They provide setup logic that can be shared across multiple tests:

import pytest

@pytest.fixture
def sample_user():
    return {"name": "Alice", "age": 30, "email": "alice@example.com"}

def test_user_name(sample_user):
    assert sample_user["name"] == "Alice"

def test_user_age(sample_user):
    assert sample_user["age"] == 30

Fixtures can also handle teardown using yield:

@pytest.fixture
def db_connection():
    conn = create_db_connection()
    yield conn
    conn.close()  # Teardown runs after the test

Parametrize: Test Multiple Inputs Cleanly

Instead of writing a separate test for each input, use @pytest.mark.parametrize:

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

Testing Exceptions

Use pytest.raises to assert that specific exceptions are raised:

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

Useful Command-Line Options

OptionEffect
-vVerbose output (shows each test name)
-xStop after first failure
-k "pattern"Run tests matching a name pattern
--tb=shortShorter traceback format
--covMeasure code coverage (requires pytest-cov)

Next Steps

Once you're comfortable with the basics, explore these pytest plugins:

  • pytest-cov — code coverage reporting
  • pytest-mock — easy mocking integration
  • pytest-xdist — parallel test execution
  • pytest-asyncio — testing async code

Testing is an investment that pays dividends every time you refactor or ship new features. Start with pytest and build the habit early.