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_*.pyor*_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:
- Functions that parse or transform input
- Functions that make decisions (conditionals, filtering)
- 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
- /tutorials/intermediate-python/testing-with-pytest/ , more pytest patterns and fixtures
- /guides/pytest-basics/ , pytest configuration and plugins
- /guides/mocking-with-pytest/ , isolating tests by mocking external dependencies
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.