Skip to content

pytest vs unittest: Which Should You Use?

pytest and unittest both run Python tests. unittest ships in the standard library and organizes tests as classes; pytest is a third-party package that lets tests be plain functions.

Start with pytest

Use pytest for almost every new project. It requires less code per test, reports failures in more detail, and its fixture system and plugins hold up as a suite grows. The Python ecosystem has largely settled on it: Django, Flask, FastAPI, pandas, NumPy, and SQLAlchemy all test with pytest or ship first-class pytest support.

One reason genuinely calls for running tests with unittest instead of pytest: you cannot install third-party packages, so the standard library is all you have. Everything else people cite dissolves on inspection. An existing unittest suite runs under pytest unchanged, so “we already use unittest” is a reason to keep those tests, not to keep the runner. Even a preference for class-based tests is covered, because pytest groups tests in plain classes and runs unittest.TestCase subclasses, setUp/tearDown and all.

How the two frameworks differ

Writing assertions

unittest uses a class per group of tests and a named method for each check:

import unittest

class TestAdd(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)

pytest uses a plain function and Python’s built-in assert:

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

The payoff shows up on failure. pytest rewrites assert statements to report the operands, so a broken check explains itself:

$ pytest
>       assert add(2, 3) == 6
E       assert 5 == 6

unittest reports the same failure as AssertionError: 5 != 6. On two integers the difference is small. On a mismatched dict or a long list, pytest’s introspection shows exactly which element diverged, while unittest sends you to the method’s docstring or a custom message.

Managing setup and teardown

unittest shares setup through setUp and tearDown methods that run before and after every test in a class:

class TestApi(unittest.TestCase):
    def setUp(self):
        self.client = ApiClient()

    def tearDown(self):
        self.client.close()

pytest uses fixtures: functions a test requests by naming them as parameters. A fixture can be scoped to a function, class, module, or session, so expensive setup runs once and is shared:

import pytest

@pytest.fixture
def client():
    c = ApiClient()
    yield c
    c.close()

def test_call(client):
    assert client.get().status_code == 200

Fixtures compose (one fixture can request another) and live in a shared conftest.py without inheritance. That composability is the feature that teams miss most after moving away from setUp/tearDown.

Discovering and selecting tests

Both frameworks auto-discover tests, but pytest’s selection is finer-grained. pytest collects files named test_*.py, functions named test_*, and classes named Test*, then lets you narrow a run by node id, substring, or marker:

pytest tests/test_api.py::test_login   # one test
pytest -k "login"                       # substring match
pytest -m slow                          # tests marked @pytest.mark.slow

unittest discovers TestCase subclasses and supports python -m unittest discover, but has no built-in equivalent of -k or markers for ad-hoc selection.

Extending with plugins

pytest has a plugin ecosystem that unittest lacks. Parallel runs come from pytest-xdist, coverage from pytest-cov, and dozens more cover snapshots and property-based testing. See essential pytest plugins for the ones worth installing early. Parameterizing a test across inputs is built in:

@pytest.mark.parametrize("n,expected", [(2, 4), (3, 9)])
def test_square(n, expected):
    assert n * n == expected

unittest covers the same ground with subTest and third-party runners, but the batteries are not included the way pytest’s are.

When to run with unittest instead

One situation genuinely calls for running tests with unittest rather than pytest: the runtime forbids third-party packages, so the standard library is all you have (a locked-down build, a security policy, a minimal container). That is the whole list.

Two things people expect to be on that list are not. A preference for class-based tests does not qualify, because pytest supports plain test classes and runs unittest.TestCase subclasses with setUp/tearDown intact. A legacy unittest suite does not qualify either, since pytest runs it unchanged. unittest is not deprecated and stays a stable, maintained choice under that one constraint; everywhere else, pytest’s lower boilerplate pays off within the first dozen tests.

You don’t have to choose all at once

pytest runs unittest.TestCase subclasses with no changes. Point pytest at a unittest suite and it discovers and executes those classes alongside any plain-function tests:

$ pytest -q
..                                    [100%]
2 passed in 0.01s

That one-way compatibility is the migration path: adopt pytest as the runner today, keep every existing unittest test green, and write new tests as plain functions. Convert old classes only when you touch them. The reverse does not hold, since python -m unittest ignores pytest’s function-style tests, so once pytest is the runner there is little reason to go back.

To set up pytest from an empty project, follow Setting up testing with pytest and uv.

Learn More

Last updated on