Python unittest vs pytest: Which Should You Use?

unittest and pytest are both used to test Python code, but they feel different in daily work. unittest is included in the standard library and uses class-based test cases. pytest is a third-party framework with simple function tests, powerful fixtures, and a large plugin ecosystem.

The right choice depends on the project. Use unittest when standard-library availability, xUnit-style classes, or existing enterprise patterns matter. Use pytest when you want concise tests, expressive assertions, fixtures, parametrization, and plugin support.

The official Python documentation covers unittest, and the official pytest documentation covers pytest.

If a project already has hundreds of tests in one framework, consistency is often more valuable than switching. New tests should match the current style unless there is a clear migration plan.

Both frameworks can run reliable automated tests. The practical difference is developer ergonomics, dependency policy, and ecosystem needs.

For small scripts, either framework is fine. For libraries and applications, think about how tests will be run by contributors, CI systems, editors, and release scripts. A test framework is part of the project’s workflow, not just a syntax preference.

pytest is often easier for new test files because plain functions and plain assertions reduce boilerplate. unittest can be easier in environments that forbid extra test dependencies or already rely on unittest.mock, TestCase, and class-based setup.

Write A unittest Test

unittest tests usually inherit from unittest.TestCase and use assertion methods.

import unittest

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

class AddTests(unittest.TestCase):
    def test_adds_numbers(self):
        self.assertEqual(add(2, 3), 5)

result = AddTests("test_adds_numbers").run()
print(result.wasSuccessful())

This style is explicit and familiar to teams that have used xUnit-style tools in other languages. It also works without installing any external package.

That standard-library availability matters for system scripts, internal tools, and restricted production environments. A developer can clone the code and run tests with Python alone.

Write A pytest-Style Test

pytest commonly uses plain test functions and normal assert statements.

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

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

test_adds_numbers()
print("passed")

pytest rewrites assertions when it runs tests, so failures usually show useful expression details. The plain function style keeps small tests compact.

The tradeoff is that pytest is an additional dependency. That is usually acceptable for development and CI, but it should be declared in the project’s development requirements so every contributor uses a known version.

Compare Test Discovery Names

Both frameworks rely on naming conventions. Test files and test functions should be named clearly.

test_names = [
    "test_user_login",
    "test_total_includes_tax",
    "helper_format_user",
]

discovered = [name for name in test_names if name.startswith("test_")]

print(discovered)

For most projects, use filenames such as test_orders.py and function or method names starting with test_. Clear names make failures easier to scan in CI output.

Discovery rules should be documented in the project README or contribution guide. If contributors cannot predict which tests run, they will miss failures locally and only see them in CI.

Choose Fixtures Or setUp

unittest often uses setUp() methods. pytest commonly uses fixtures. Both approaches prepare test data before assertions run.

class CartTestData:
    def setUp(self):
        self.items = [10, 20, 30]

case = CartTestData()
case.setUp()

print(sum(case.items))

Use setup methods when a class of tests shares the same state. Use fixtures when you want reusable setup that can be shared across many test modules.

Fixtures are one of pytest’s biggest strengths. They can prepare files, temporary directories, clients, sample data, and cleanup behavior without forcing every test into the same class hierarchy.

Use Parametrized Cases

pytest has first-class parametrization. In unittest, similar coverage is often written as loops with subtests.

def is_even(number):
    return number % 2 == 0

cases = [(2, True), (3, False), (10, True)]

for value, expected in cases:
    assert is_even(value) is expected

print("all cases passed")

Parametrized tests reduce duplication when the same behavior should be checked across several inputs. They also make it easier to add edge cases over time.

In unittest, subTest() can cover a similar need. In pytest, @pytest.mark.parametrize makes the cases visible in test output and can report exactly which input failed.

Pick Based On Project Needs

A simple decision table is often enough for teams choosing a framework.

choices = {
    "standard library only": "unittest",
    "concise new tests": "pytest",
    "large plugin ecosystem": "pytest",
    "existing xunit codebase": "unittest",
}

for reason, framework in choices.items():
    print(reason, "->", framework)

Many projects use pytest to run tests that include some unittest-style classes. That can be a practical transition path because pytest can discover many unittest tests while new tests use pytest style.

Migration does not have to happen all at once. A team can keep existing unittest classes, add pytest as the runner, and write new tests in pytest style. Later, old tests can be simplified when they are touched for real maintenance.

In short, choose unittest for standard-library availability and class-based structure. Choose pytest for concise tests, fixtures, parametrization, plugins, and clearer assertion output. More important than the tool is that tests are readable, fast enough to run often, and included in CI.