The pytest framework is the most widely used testing tool in the Python ecosystem. It replaces unittest's class structure with plain functions and the built-in assert statement. Less boilerplate, better output.
Install pytest with pip:
pip install pytestOnce installed, the pytest command discovers and runs your tests. It finds files named test_*.py, functions starting with test_, and classes starting with Test. There is no need to subclass anything.
If you have worked through Python unittest explained, the concepts will feel familiar. pytest simply expresses them with less ceremony.
Plain assert statements
The most visible difference from unittest is how you write assertions. Instead of self.assertEqual(a, b), you write assert a == b. pytest inspects the assertion at runtime and, when it fails, prints the exact values involved in the comparison.
def add(a, b):
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2Run these with the pytest command and pytest finds the file, runs both functions, and reports the results. When a test passes, pytest prints a green dot. When it fails, it shows the assertion that failed along with every subexpression value.
Compare this to a deliberately broken test:
def test_add_broken():
assert add(2, 2) == 5pytest output for this failure shows the computed value on each side of the comparison and the intermediate expression that produced it. This level of detail requires no extra work from you.
Fixtures for setup and teardown
In unittest, setUp and tearDown manage test data. pytest replaces both with a single concept: fixtures. A fixture is a function decorated with @pytest.fixture that returns a resource your test needs. Tests request fixtures by including the fixture name as a parameter.
import pytest
@pytest.fixture
def sample_list():
return [1, 2, 3, 4, 5]
def test_list_length(sample_list):
assert len(sample_list) == 5
def test_list_sum(sample_list):
assert sum(sample_list) == 15Each test receives a fresh copy of the list. If one test modifies it, the next test still starts with the original five numbers. pytest calls the fixture function once per test by default, maintaining isolation automatically.
For fixtures that need cleanup, use yield instead of return. Everything before yield is setup; everything after is teardown.
@pytest.fixture
def temp_file():
path = "/tmp/test_data.txt"
with open(path, "w") as f:
f.write("temporary content")
yield path
import os
os.remove(path)The fixture creates a file, yields the path to the test, and removes the file afterward regardless of whether the test passed or failed.
Fixture scopes
By default, a fixture runs once per test function. Change this with the scope parameter to share expensive resources across multiple tests.
@pytest.fixture(scope="module")
def database_connection():
conn = create_test_database()
yield conn
conn.close()A module-scoped fixture runs once for all tests in the file. Supported scopes are function (the default), class, module, package, and session. Choose the narrowest scope that meets your performance needs.
Parametrized tests
When you have the same test logic with different inputs, the @pytest.mark.parametrize decorator runs the same function once for each set of arguments.
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
])
def test_add_parametrized(a, b, expected):
assert a + b == expectedpytest runs this test three times and reports each run as a separate result with the argument values in the test name. When one case fails, pytest reports that specific case while continuing to run the remaining ones.
Parametrize also works with fixtures. Combine a parametrized input with a fixture that provides shared setup for each variation.
Markers for tagging and filtering
pytest markers attach metadata to tests so you can selectively run or skip groups of them. The built-in markers include skip, skipif, and xfail.
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
assert connect_to_service() is not None
@pytest.mark.skipif(sys.version_info < (3, 11), reason="Requires Python 3.11+")
def test_new_syntax():
assert True
@pytest.mark.xfail(reason="Known bug in the date parser")
def test_iso_date_parsing():
assert parse_date("2026-07-13") == "July 13, 2026"Custom markers let you group tests by category. Define them in your pyproject.toml file, then apply them to test functions and use the -m flag to run specific groups:
pytest -m slow # Run only slow tests
pytest -m "not slow" # Skip slow testsThis gives you fine-grained control over which tests run in different contexts, such as running fast smoke tests on every commit and the full suite before a release.
Running unittest tests with pytest
pytest can discover and run unittest TestCase classes without modification. Install pytest and run it against a project that uses unittest, and it finds your test classes, executes them, and reports results using pytest's output format.
This compatibility means you can keep existing unittest tests and write new ones in pytest style. The article on organizing and running Python tests covers project layout and test execution strategies for both frameworks.
Rune AI
Key Insights
- pytest lets you write tests as plain functions with the built-in assert statement and provides detailed failure output automatically.
- Fixtures replace setUp and tearDown with a more flexible system of dependency injection, supporting function, class, module, and session scopes.
- @pytest.mark.parametrize runs the same test logic with multiple input sets and reports each case independently.
- pytest runs unittest TestCase classes without modification, so you can adopt it incrementally in existing projects.
- Install pytest with pip install pytest and run it with the pytest command from your project root.
Frequently Asked Questions
Do I have to rewrite my unittest tests to use pytest?
What is the difference between a fixture and setUp in unittest?
How do I pass multiple inputs to the same test in pytest?
Conclusion
pytest is not just a unittest replacement; it is a design that makes tests shorter, clearer, and easier to maintain. Plain assert statements, dependency injection via fixtures, parametrized test runs, and marker-based organization all reduce the friction of writing and maintaining tests. If you are starting a new project, start with pytest.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.