pyguides

pytest Basics: Writing Your First Python Tests with pytest

Intro context

pytest basics start with a single idea: testing in Python should feel cheap. pytest is the standard way to write tests in Python because it finds your test functions automatically, gives you readable output, and doesn’t impose a particular test style. If you’ve been checking code with scripts and print statements, learning pytest basics is the upgrade that makes testing sustainable.

This tutorial walks through installing pytest, writing your first test function, reading the runner’s output, working with plain assert statements, parametrizing tests, and grouping shared setup. Each section builds on the last so you can go from zero tests to a small, working suite. For deeper topics once you finish here, see the pytest basics guide and the fixtures and parametrize tutorial.

Installation

pip install pytest

Verify it works:

pytest --version

That’s it. No configuration files needed to get started.

Your first test

Create a file called test_math.py:

def square(n):
    return n * n

def test_square_of_positive():
    assert square(3) == 9

def test_square_of_zero():
    assert square(0) == 0

def test_square_of_negative():
    assert square(-4) == 16

Run it:

pytest test_math.py

pytest discovers any function starting with test_ and runs it. No test classes required, no inheritance hierarchies, no boilerplate.

Reading the output

==== test session start ====
collected 3 items

test_math.py::test_square_of_positive PASSED
test_math.py::test_square_of_zero PASSED
test_math.py::test_square_of_negative PASSED

==== 3 passed in 0.05s ====

When a test fails, pytest shows exactly what went wrong:

def test_square_of_negative():
    assert square(-4) == 15  # wrong expected value
FAILED test_math.py::test_square_of_negative
>       assert 16 == 15
>       +  where 16 = square(-4)

pytest prints the actual value and the expected value so you can see the mismatch immediately.

Assertions

pytest assertions use Python’s built-in assert. That’s deliberate , you don’t learn a special assertion API:

def test_string_operations():
    assert "hello".upper() == "HELLO"
    assert "World".lower() == "world"
    assert "pytest".isalpha() == True

def test_list_operations():
    nums = [1, 2, 3]
    nums.append(4)
    assert nums[-1] == 4
    assert len(nums) == 4

pytest shows more detail when assertion messages would be unclear, and automatically includes the values of any variables in the expression.

Testing a real function

Real functions do more than one thing. Test each piece:

# sales.py
def apply_discount(price, discount_percent):
    """Return the price after applying a percentage discount."""
    if discount_percent < 0 or discount_percent > 100:
        raise ValueError("Discount must be between 0 and 100")
    return price * (1 - discount_percent / 100)
# test_sales.py
import pytest
from sales import apply_discount

def test_apply_discount_ten_percent():
    assert apply_discount(100, 10) == 90.0

def test_apply_discount_full_discount():
    assert apply_discount(50, 100) == 0.0

def test_apply_discount_zero_discount():
    assert apply_discount(100, 0) == 100.0

def test_apply_discount_invalid_too_high():
    with pytest.raises(ValueError):
        apply_discount(100, 110)

def test_apply_discount_invalid_negative():
    with pytest.raises(ValueError):
        apply_discount(100, -10)

pytest.raises catches expected exceptions , it fails if the function doesn’t raise the exception you expected.

Running tests

# Run all tests in the current directory
pytest

# Run a specific file
pytest test_sales.py

# Run a specific test function
pytest test_sales.py::test_apply_discount_ten_percent

# Run with verbose output
pytest -v

# Run with even more detail
pytest -vv

-v shows each test on its own line with PASSED or FAILED. -vv adds the full assertion diff for failures.

Test discovery

pytest finds tests by following these rules:

  • Files named test_*.py or *_test.py
  • Functions named test_*
  • Classes named Test*

This means you can drop test files anywhere in your project and pytest will find them:

pytest tests/          # runs everything under tests/
pytest src/            # runs everything under src/
pytest                 # runs from current directory

Using fixtures for setup

If your tests need the same data or object, use a fixture:

import pytest

@pytest.fixture
def sample_dataframe():
    import pandas as pd
    return pd.DataFrame({
        "name": ["Alice", "Bob", "Carol"],
        "revenue": [100, 200, 150]
    })

def test_total_revenue(sample_dataframe):
    assert sample_dataframe["revenue"].sum() == 450

def test_average_revenue(sample_dataframe):
    assert sample_dataframe["revenue"].mean() == 150

The fixture is called once per test that uses it. pytest passes the returned value as an argument to each test function.

Testing float comparisons

Money and percentages involve floats, and float comparison needs a tolerance:

def test_discount_with_rounding():
    result = apply_discount(33.33, 10)
    assert abs(result - 29.997) < 0.001

pytest includes pytest.approx for cleaner float comparison:

def test_discount_with_rounding():
    result = apply_discount(33.33, 10)
    assert result == pytest.approx(29.997, rel=0.001)

pytest.approx succeeds when the value is within the relative tolerance.

Marking and selecting tests

Mark tests to run only certain groups:

import pytest

@pytest.mark.slow
def test_full_pipeline():
    ...  # takes 30 seconds

@pytest.mark.fast
def test_quick_validation():
    ...

Run only fast tests:

pytest -m fast

Run tests that are NOT slow:

pytest -m "not slow"

Common built-in markers: @pytest.mark.skip, @pytest.mark.xfail.

What to test

The honest answer: start with the functions that have already broken in production. Once those are covered, add tests for:

  1. Functions that parse or transform input
  2. Functions that make decisions (conditionals, filtering)
  3. Functions that interact with external systems (files, network)

Don’t test trivial getter/setter code. Don’t test language features. Test the code that actually does something.

See also

Where to go next

Once you’re comfortable with pytest basics, dig deeper into reusable setup with the fixtures and parametrize tutorial, and learn how to fake collaborators with the testing mocking tutorial. For measuring how thoroughly your suite exercises the code, see the coverage and mutation tutorial.