ScreenshotNeo

BlogComparisons

pytest vs. unittest: Which Python Testing Framework Should You Choose?

Compare pytest and unittest by test style, fixtures, parametrization, discovery, and migration. Choose the framework that fits your team's workflow.

By the ScreenshotNeo team4 October 20269 min read

Short answer: Choose pytest if you want concise test functions, plain assert statements with detailed failure explanations, reusable fixtures, and built-in parametrization. Choose unittest if your team prefers a framework included in Python, class-based TestCase organization, explicit assertion methods, and its built-in suites and runner. Neither is a universal winner; choose the conventions your team will use consistently.

A practical middle path is to run many existing unittest suites with pytest, then adopt pytest features gradually where they help. Pytest’s fixtures and parametrization do not work as ordinary injected test arguments inside unittest.TestCase methods.

1. pytest vs unittest at a glance

Question pytest unittest
Installation Install separately with pip. Included in Python’s standard library.
Typical test shape Functions such as def test_add():; plain assert. Methods beginning with test on a unittest.TestCase subclass; methods such as assertEqual().
Shared setup Fixtures provide values and resources, can depend on other fixtures, and can manage cleanup. setUp() and tearDown() methods, with class and module setup patterns too.
Repeated cases Built-in @pytest.mark.parametrize and fixture parametrization. Subtests can group related checks; the reviewed documentation does not describe an equivalent decorator-style parametrization feature.
Discovery and runner Pytest command-line discovery and options; can collect many unittest tests. python -m unittest supports command-line execution and discovery.
Best fit Teams that value concise functions, composable fixtures, parametrization, and pytest’s plugin architecture. Teams that value a standard-library-only setup and an explicit class, suite, and runner model.

The official documentation describes features and usage, not a general head-to-head speed or productivity winner. If runtime matters, benchmark representative tests in your own environment.

2. Writing the same test both ways

Suppose a function normalizes a name. The examples below are complete minimal tests; replace the sample function with an import from your application when integrating them.

pytest

# test_names.py
def normalize_name(value):
    return " ".join(value.strip().split()).title()


def test_normalize_name():
    assert normalize_name("  ada   lovelace ") == "Ada Lovelace"

Install and run:

python -m pip install -U pytest
python -m pytest

Pytest rewrites ordinary assertions to provide useful context when they fail, so the assertion can stay close to normal Python.

unittest

# test_names.py
import unittest


def normalize_name(value):
    return " ".join(value.strip().split()).title()


class NormalizeNameTests(unittest.TestCase):
    def test_normalize_name(self):
        self.assertEqual(normalize_name("  ada   lovelace "), "Ada Lovelace")


if __name__ == "__main__":
    unittest.main()

Run it using discovery:

python -m unittest

Both styles can express the same behavior. The choice changes how tests are organized and how setup, repeated data, and tooling fit together.

3. Fixtures versus setup and teardown

Fixtures are one of pytest’s main workflow differences. A fixture can construct a value or resource, be requested by name by tests or other fixtures, and clean up after use. Scope lets a fixture be created at a function, class, module, package, or session level, depending on the resource lifecycle.

# test_users.py
import pytest


@pytest.fixture
def user():
    return {"name": "Ada", "active": True}


def test_active_user(user):
    assert user["active"] is True
    assert user["name"] == "Ada"

For resources that need cleanup, a fixture can yield the resource and perform teardown after the yield:

@pytest.fixture
def temporary_connection():
    connection = open_connection()
    try:
        yield connection
    finally:
        connection.close()

open_connection() is application-specific; replace it with the constructor for the resource under test. Fixtures make dependencies visible in a test’s parameters, while the fixture controls creation and cleanup.

In unittest, per-test lifecycle work usually lives in methods on the test case:

import unittest


class ConnectionTests(unittest.TestCase):
    def setUp(self):
        self.connection = open_connection()

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

    def test_connection_is_open(self):
        self.assertTrue(self.connection.is_open())

Again, open_connection() represents application code. Use addCleanup() when cleanup should be registered immediately after a resource is created, so cleanup still runs if later setup work fails. Choose fixture scopes or setup hooks based on the real lifetime and isolation needs of the resource; sharing state can reduce repeated setup but can also make tests depend on one another.

4. Repeated inputs: parametrization and subtests

When a behavior has several input/output examples, pytest can run one test function for each case:

import pytest


@pytest.mark.parametrize(
    "raw, expected",
    [
        ("ada", "Ada"),
        ("  grace hopper ", "Grace Hopper"),
        ("", ""),
    ],
)
def test_normalize_name(raw, expected):
    assert normalize_name(raw) == expected

Each parameter set is reported as a separate test case. Fixtures can also be parametrized when the same test should run against multiple resources or configurations.

With unittest, subTest can identify individual cases while keeping a loop in one method:

import unittest


class NormalizeNameTests(unittest.TestCase):
    def test_examples(self):
        examples = [
            ("ada", "Ada"),
            ("  grace hopper ", "Grace Hopper"),
            ("", ""),
        ]
        for raw, expected in examples:
            with self.subTest(raw=raw):
                self.assertEqual(normalize_name(raw), expected)

Use pytest parametrization when separate case reporting and decorator-style data tables suit the suite. Use subtests when your team wants to stay with unittest and group related checks in a method.

5. Discovery, naming, and running tests

For a basic pytest project, name test files test_*.py or *_test.py and test functions or methods with a test prefix. Run discovery from the project root with python -m pytest. You can select a file or a node such as a class or test function:

python -m pytest tests/test_names.py
python -m pytest tests/test_names.py::test_normalize_name
python -m pytest -q

For unittest, use discoverable test files and methods, then run discovery with:

python -m unittest
python -m unittest discover -s tests -p "test_*.py"
python -m unittest tests.test_names.NormalizeNameTests.test_normalize_name

Check the runner’s help for options available in the installed Python or pytest version. Discovery depends on paths, names, and package layout, so a test that works when invoked directly may not be found from the project root.

Python-version detail: the Python 3.14 documentation says namespace packages are supported again as the discovery start directory, while discovery still does not descend into subdirectories without __init__.py. If relying on a particular package layout, consult the documentation for the Python version used in CI.

6. Can pytest run unittest tests?

Usually, yes. Pytest can collect and run most tests written with unittest.TestCase. This lets a team use pytest’s runner and reporting without immediately rewriting its test classes.

Keep the boundary clear: pytest fixture arguments cannot generally be added to methods on a unittest.TestCase subclass, and the usual pytest parametrization decorator is not available there as it is for pytest test functions. Some pytest facilities have documented integration patterns, but a TestCase method does not become an ordinary pytest function just because pytest runs it.

A low-risk migration sequence is:

  1. Install pytest in the development environment and run the existing suite with python -m pytest.
  2. Fix collection or test failures while leaving test structure intact.
  3. Use pytest for new function-style tests where that style helps.
  4. Convert an existing test class only when you need pytest fixtures, parametrization, or another pytest feature.
  5. Keep the suite’s runner and conventions documented so local runs and CI agree.

7. Installation and project configuration

unittest needs no separate framework installation. Pytest is a separate package; the current getting-started instructions show installation with pip. Pin and manage it using the dependency workflow your project already uses, especially when CI needs reproducible environments.

python -m pip install -U pytest
python -m pytest

Pytest supports command-line options and project configuration. Start with the simplest configuration that matches the repository rather than copying a large configuration file. For example, a minimal pytest.ini can name the test directory:

[pytest]
testpaths = tests

Then run python -m pytest from the repository root. If the project needs additional settings, consult the pytest configuration reference for the installed version. Avoid configuring discovery rules that exclude tests unintentionally.

8. Which Python testing framework should you choose?

  • Choose pytest for a new project if concise functions, plain assertions, fixture composition, and built-in parametrization match the team’s habits.
  • Choose unittest if the standard library constraint matters, or the team prefers explicit TestCase classes, assertion methods, suites, and runner conventions.
  • For many repeated cases, pytest’s parametrization is a direct built-in fit; unittest subtests are a standard-library option.
  • For resource lifecycle management, compare pytest fixture dependencies and scopes with unittest setup, teardown, and cleanup hooks using your actual resources.
  • For an existing unittest codebase, try pytest as a runner before considering a rewrite.
  • If speed decides the choice, benchmark representative test runs with the same interpreter, dependencies, collection scope, and environment. The documentation reviewed does not establish a general speed winner.

Pytest’s project overview also describes a plugin architecture. Its plugin count changes over time, so treat any count as a dated project-maintained figure rather than independent evidence.

9. Troubleshooting common problems

Symptom Likely cause What to do
pytest: command not found Pytest is not installed in the active environment or its script directory is not on PATH. Activate the project environment, install with python -m pip install pytest, and run python -m pytest.
Pytest collects zero tests File, class, or function names do not match discovery conventions, or the command starts at the wrong directory. Run from the repository root; check test names and use an explicit test path to diagnose collection.
Unittest reports zero tests Discovery pattern or start directory does not match the layout, or test methods lack the test prefix. Try python -m unittest discover -s tests -p "test_*.py" and verify package layout for your Python version.
A pytest fixture argument errors in a TestCase method Fixture injection is not generally supported as a method argument for unittest.TestCase. Use setUp/tearDown, a documented integration pattern, or convert that test to a pytest function.
Import works in one invocation but fails in discovery The current working directory, package structure, or Python path differs between invocations. Run from the project root, use a consistent package layout, and configure the environment identically in local runs and CI.
Tests pass alone but fail together Shared mutable state, leaked resources, or order dependence. Make cleanup reliable, isolate test data, and avoid relying on execution order.
A parametrized case is hard to identify Case values are not descriptive in test output. Use readable parameter values or pytest’s parameter IDs when appropriate.

10. Performance, reliability, and cost

Performance: Neither framework should be selected on an assumed universal speed advantage. Collection time, imports, fixtures, I/O, database setup, and test count can dominate the framework overhead. Compare equivalent runs in the project’s own CI-like environment if elapsed time is important.

Reliability: Both frameworks can support reliable suites. Reliability depends on deterministic tests, isolated state, predictable cleanup, controlled external dependencies, and consistent discovery in developer and CI environments. Fixture scopes and setup hooks are lifecycle tools; neither removes the need to reason about state sharing.

Cost: The unittest framework comes with Python. Pytest adds a separately installed dependency to the environment. Account for dependency review and maintenance according to your team’s normal package policy; the cited documentation does not provide a monetary cost comparison.

11. Or skip the browser setup

This framework comparison is about Python testing, but if your tests or documentation workflow also need website screenshots, ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call API returns an image or PDF; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free account and get 1,000 screenshots a month with no card.

12. FAQ

Should I use pytest or unittest for a new project?

Use the style your team will maintain. Pytest suits teams that want fixtures and parametrization with concise functions; unittest suits teams that want a standard-library framework and class-based organization.

Is pytest faster than unittest?

The documentation reviewed does not establish a general answer. Benchmark the same representative tests under the same environment if runtime is decisive.

Can I keep unittest and install pytest later?

Yes. Pytest can run most unittest-style suites, so you can evaluate its runner before changing how tests are authored.

Does unittest require installing a package?

No. It is part of Python’s standard library.

Can I use pytest fixtures with every unittest test?

No. Fixture injection and ordinary pytest parametrization are not generally available as arguments and decorators on unittest.TestCase methods.

Does either framework guarantee better test coverage?

No. Coverage depends on which behaviors and failure cases the tests exercise, not the framework choice alone.

Sources