Browse the Knowledge Hub211 resources
pytest vs unittest, and why it is rarely either or
unittest ships with Python. pytest is a dependency that most Python teams choose anyway. This compares them on setup and fixtures, assertions, parametrisation, plugins and CI, explains what carries over when pytest runs a unittest suite, and says which one to pick.
- options
- 2
- dimensions
- 23
- checked
- October 2026
The short answer
Our recommendation
Use pytest for almost any Python project. Tests are plain functions with plain assert statements, failures show the values involved without an assertion API, fixtures handle setup more flexibly than setUp and tearDown, parametrisation is built in, and a large plugin ecosystem covers parallel runs, coverage, retries and browser automation. It is free under MIT and needs Python 3.10 or later in its current major.
unittest still wins in one clear case: when you cannot add a dependency at all. It ships with every Python installation, so code inside the standard library, tightly controlled environments, single file scripts and some packaging situations use it because it is already there. It is also the familiar xUnit shape for engineers coming from JUnit, which is a weaker reason but a real one.
The choice is less final than it looks, because pytest runs unittest.TestCase suites unchanged, including setUp, setUpClass, setUpModule, skip decorators and, since pytest 9.0, subTest. A team on unittest can switch the runner today, keep every existing test, and write new tests in pytest style. The move is one way, though: unittest cannot collect plain pytest test functions.
Side by side
23 dimensions that actually decide it
Download or copy the table if you are writing this up for a decision record.
pytest
The standard third party Python test framework: plain assert, fixtures injected by name, built in parametrisation and a large plugin ecosystem.
Open source, MIT, free
unittest
Python’s standard library xUnit framework: TestCase classes, assert methods and setUp and tearDown, available wherever Python is.
Part of Python, PSF licence
| Dimension | pytest | unittest |
|---|---|---|
| Writing tests | ||
Test style Shorter tests get read, reviewed and maintained. | Plain functions, or classes without a base class. | Methods on unittest.TestCase subclasses. |
Assertions An assertion vocabulary is one more thing every new contributor has to learn. | Plain assert, rewritten at import to report the values compared. | assertEqual, assertIn, assertRaises and dozens of related methods. |
Failure output What a red build tells you before anyone opens a debugger. | Both sides of the comparison, with diffs for strings, lists and dictionaries. | Good diffs from assertEqual and friends. A bare assert gives little. |
Async tests | Needs a plugin such as pytest-asyncio or AnyIO’s pytest plugin. | IsolatedAsyncioTestCase built in since Python 3.8. |
Floating point and exceptions | pytest.approx and pytest.raises with a match pattern. | assertAlmostEqual and assertRaises or assertRaisesRegex. |
| Setup, test data and cleanup | ||
Setup and teardown Where suites accumulate the most hidden coupling. | Fixtures requested by argument name, teardown after yield. | setUp and tearDown per test, setUpClass and setUpModule above. |
Sharing setup across files Inheritance chains for shared setup become hard to change. | conftest.py fixtures visible to every test below that directory. | Base classes and mixins, or helper modules. |
Resource lifetime | Fixture scope: function, class, module, package or session. | Per test, per class or per module through the matching hooks. |
Parametrisation Data driven tests are where one framework is clearly ahead. | @pytest.mark.parametrize. Each case is a separate test with its own ID. | subTest loops inside one test method. |
Cleanup helpers | tmp_path, monkeypatch, capsys and caplog fixtures built in. | addCleanup, enterContext and assertLogs built in. |
| Running and selecting tests | ||
Discovery | test_*.py or *_test.py files, test functions and Test classes. | python -m unittest discover, matching test*.py files by default. |
Selecting a subset Pull request, nightly and smoke runs need different slices of one suite. | -k by name, -m by marker expression, or a node ID. | -k name patterns since Python 3.7, or a dotted test name. |
Rerunning failures | --lf reruns last failures, --ff runs them first. | Not built in. Rerun by name. |
Parallel execution The biggest lever on how long CI takes. | pytest-xdist plugin, for example -n auto. | Nothing built in. Split runs across CI jobs yourself. |
Slowest tests report | --durations, including fixture setup time. | --durations since Python 3.12. |
| Ecosystem and CI | ||
Plugins Coverage, retries and browser fixtures should not be hand written. | Hook based plugin system with a large ecosystem on PyPI. | No plugin system. Extend with custom runners and result classes. |
JUnit XML for CI | --junitxml built in. | Needs a third party runner or reporter package. |
Mocking | unittest.mock works as is, plus monkeypatch and the pytest-mock plugin. | unittest.mock, including the patch decorator and context manager. |
Browser and API automation | Playwright for Python ships a pytest plugin. Selenium suites commonly use fixtures. | Works with any driver, with setup written by hand. |
| Ownership and compatibility | ||
Dependency The one row that can override everything else. | A package to install and pin, plus any plugins. | None. Ships with Python. |
Version and requirements Checked on PyPI and python.org on 13 October 2026. | 9.1.1 on PyPI. pytest 9 needs Python 3.10 or later. | Versioned with Python. Python 3.15.0 was released on 9 October 2026. |
Running the other’s tests Decides whether a migration can be gradual. | Runs unittest.TestCase suites, except the load_tests protocol. | Cannot collect plain pytest functions or use fixtures. |
Upgrade churn | pytest 9 removed long deprecated features. Plugins must support each major. | Changes slowly, with deprecations following Python’s release cycle. |
A tick marks the option we would pick on that row alone. Rows without a tick are close enough that the choice should come from elsewhere in the table. Checked October 2026; tool releases move quickly, so verify anything you are about to spend money on.
Pick one
Which one fits your situation
Find the list that describes your team, not the tool with the longest feature table.
Choose pytest if
- You are starting a new Python project or test suite of any size.
- Tests need shared setup such as database transactions, API clients or browser sessions.
- You want data driven tests where each case is reported and rerun on its own.
- CI needs parallel runs, JUnit XML, coverage and retries without custom code.
- You drive Playwright or Selenium from Python and want ready made fixtures.
Choose unittest if
- No third party dependency is allowed, even for development.
- The code is a single script or a small library where installing a runner is overhead.
- You need async tests and cannot add a plugin.
- The suite is stable, small and nobody is adding to it.
What changes the answer
Conditions that reverse the recommendation
A recommendation with no stated limits is a guess. These are the situations where ours does not hold.
Dependencies are forbidden
Locked down environments, code destined for the standard library and some audited build chains cannot install packages. unittest is then the only choice, and it is a capable one.
The suite is finished, not growing
A small unittest suite that passes and rarely changes does not need rewriting. If you want pytest’s output and selection, just run it with pytest and leave the tests alone.
Async code with no plugins allowed
Core pytest does not run coroutine tests. IsolatedAsyncioTestCase does, so a dependency free async suite belongs in unittest.
In detail
Where the differences actually bite
The dimensions from the table, with the reasoning that would not fit in a cell.
Fixtures versus setUp: the difference that matters most
In unittest, setup belongs to the class. setUp runs before every test in a TestCase, setUpClass once per class and setUpModule once per module, and sharing setup across files means inheritance or mixins. That works well for small suites. In large ones, a base class grows to serve every test, each test pays for setup it does not need, and changing it means reading every subclass.
pytest turns setup into dependencies. A test names the fixtures it needs as arguments, pytest builds them and anything they depend on, and code after a yield runs as teardown even when the test fails. Scope is a property of the fixture, from function to session, and a conftest.py makes fixtures available to every test below it without imports. Each test declares exactly what it uses.
The cost is indirection. A fixture can be defined in any conftest.py above the test, and autouse fixtures run without being named at all. pytest’s --fixtures option lists what is available, but suites with deep conftest hierarchies still become hard to follow. Keep fixtures close to the tests that use them, and reserve autouse for setup that genuinely applies everywhere.
Assertion rewriting and why plain assert works
A plain assert in a Python script reports nothing useful on failure. pytest changes that by rewriting assert statements in test modules and conftest.py files as they are imported, so a failed comparison reports both sides, the differing keys of two dictionaries or the first mismatched item in two lists.
This is why pytest tests are shorter without being less informative. It is also a detail worth knowing: helper modules outside test files are not rewritten unless registered with pytest.register_assert_rewrite, so an assert inside a shared helper fails with far less detail than one in the test itself.
unittest takes the other route. Its assert methods carry the reporting logic, and assertEqual gives good diffs for sequences, dictionaries and long strings. The output is fine; the cost is learning and remembering which method to use, and the temptation to write assertTrue around an expression, which reports only that something was false.
Parametrisation and subTest
@pytest.mark.parametrize turns one test function into a test per argument set. Each case gets an ID in the node name, so a failure names the case, it can be rerun on its own, and individual cases can carry marks such as xfail with a reason. Fixtures can be parametrised too, which runs every dependent test against each variant, such as two database backends.
unittest’s answer is subTest, available since Python 3.4. A loop inside one test method wraps each iteration in a subTest context, and failures are reported per iteration rather than stopping at the first. It is useful, particularly when cases are only known at run time, but the cases are not separate tests: you cannot select or rerun one by name.
pytest also supports subTest inside TestCase classes as of 9.0, and 9.0 added an experimental subtests fixture for the run time case. What it does not support is @pytest.mark.parametrize on unittest.TestCase methods. The pytest documentation says parametrisation and fixture arguments do not work in TestCase subclasses and probably never will, because the two designs differ.
Plugins, parallelism and CI
pytest is built on hooks, and plugins implement them. pytest-xdist runs tests across processes with -n auto, pytest-cov wraps coverage measurement, rerun plugins add retries, and Playwright for Python ships a pytest plugin with browser and page fixtures. JUnit XML output is built into pytest with --junitxml, which most CI servers read directly.
unittest has no plugin system and no parallel runner. Parallelism means splitting the suite across CI jobs by module, and JUnit XML means adding a third party runner or result class, at which point the no dependency advantage has gone. Coverage works with either through coverage.py.
The trade-off on the pytest side is upgrade coupling. Each plugin has to support a new pytest major before you can move, and pytest 9 removed several long deprecated behaviours. Pin pytest and its plugins together and upgrade them as a set.
Before you commit
Mistakes teams make on this decision
Rewriting a working suite for style
pytest runs unittest suites unchanged. Switch the runner first, get the benefits in output and selection immediately, and only convert tests you are already editing.
Adding fixture arguments to TestCase methods
TestCase methods cannot receive fixtures as arguments, and parametrize does not apply to them. Use autouse fixtures or @pytest.mark.usefixtures inside TestCase classes, or convert the class to plain functions.
Relying on load_tests
pytest does not support unittest’s load_tests protocol. Suites that build their test list in load_tests need that logic moved to markers, parametrisation or collection hooks before they run under pytest.
Asserts in helper modules under pytest
Assertion rewriting applies to test modules and conftest.py only. Register shared helper modules with pytest.register_assert_rewrite, or their failures lose the detailed output.
Session fixtures under pytest-xdist
Each xdist worker is a separate process, so a session scoped fixture runs once per worker. Make shared setup idempotent or give each worker its own resource.
Building a Python test suite?
QAble designs automation suites for APIs, web and data pipelines. We will review your current tests and recommend a structure your team can keep up.
Test automation servicesSwitching cost
Moving from unittest to pytest
Start by changing only the runner. Install pytest, run it from the project root, and check that the test count matches what python -m unittest reports. pytest collects TestCase subclasses and honours setUp, tearDown, setUpClass, setUpModule, skip decorators and, since 9.0, subTest. Anything built on load_tests will not run and needs moving first.
Then write all new tests in pytest style and convert old ones only when you touch them for another reason. pytest-dev’s unittest2pytest tool rewrites self.assertEqual and its relatives into plain assert statements and assertRaises blocks into pytest.raises. Turning setUp methods into fixtures is a manual step, and it is the step that pays off, because it is where shared state and hidden coupling get removed.
Do not convert everything at once. A mixed suite runs fine under pytest, and a big bang rewrite of hundreds of TestCase classes produces a large diff that is hard to review and easy to get subtly wrong. Convert by module, keep both styles green throughout, and delete tests that no longer guard anything.
Questions
Questions people ask before choosing
Is pytest better than unittest?
For most projects, yes. pytest tests are shorter, failures show more detail, fixtures are more flexible than setUp and tearDown, parametrisation is built in, and plugins cover parallel runs, coverage and browser automation. unittest is better only where you cannot add a dependency, or for async tests where no plugin is allowed. pytest also runs existing unittest suites, so choosing it costs nothing already written.
Can pytest run unittest tests?
Yes. pytest collects unittest.TestCase subclasses and supports almost all unittest features, including setUp and tearDown, setUpClass, setUpModule, skip decorators and, since pytest 9.0, subTest. The load_tests protocol is not supported. Inside TestCase classes, pytest marks and autouse fixtures work, but fixture arguments and parametrisation do not.
Can I use pytest and unittest together?
Yes, and that is the normal way to migrate. Run everything with pytest: existing TestCase classes keep working and new tests are written as plain functions with fixtures. The reverse does not work, because unittest’s runner cannot collect plain pytest test functions, so once you write pytest style tests, pytest is your runner.
Is pytest free?
Yes. pytest is open source under the MIT licence, with no paid edition. It is maintained by volunteers in the pytest-dev organisation and funded by donations. Organisations that need a commercial support agreement can buy one through Tidelift. unittest is part of Python itself, under the Python Software Foundation licence.
Why is unittest still in the standard library?
Because Python needs a test framework that is always available. The standard library’s own tests, environments that cannot install packages and quick scripts all rely on it. It also keeps getting small improvements: -k filtering arrived in Python 3.7, IsolatedAsyncioTestCase in 3.8, enterContext in 3.11 and --durations in 3.12.
Does unittest support parametrised tests?
Not as separate tests. unittest offers subTest, available since Python 3.4, which reports each iteration of a loop separately inside one test method. The cases cannot be selected or rerun individually by name. pytest’s @pytest.mark.parametrize creates a separate test per case with its own ID, which is the main reason data driven suites move to pytest.
How do I run unittest tests in parallel?
unittest has no built in parallel runner. The simplest route is to run the same suite with pytest and add the pytest-xdist plugin, then use -n auto to spread tests across processes. Tests must not share state, and setUpClass runs once in each worker that receives tests from that class. Without pytest, split modules across separate CI jobs.
Which should a beginner learn first?
pytest. Writing a test is just writing a function with an assert, and the failure output explains what went wrong. It is also what most Python automation, including Playwright and Selenium suites, is built on. Learn enough unittest to read TestCase classes, because you will meet them in older codebases, and know that pytest runs them.
How we compared
How this comparison was put together
This comparison comes from evaluation and documentation review rather than long-running client suites. We worked from the pytest documentation and changelog, the unittest chapter of the Python documentation and the PyPI and python.org release records, and wrote the same small suite in both to check how each behaves on failure, setup and selection.
Rows without a tick are close enough that they should not decide anything. Where pytest needs a plugin to match a built in unittest feature, or the other way round, the row says so, because a dependency is the one thing unittest genuinely saves you.
Versions and behaviours were checked on 13 October 2026, against pytest 9.1.1 and Python 3.15. If something here is wrong for your setup, tell us and we will correct it.
Tell us where we are wrongComparisons
More comparisons
Playwright vs Cypress
ComparisonArchitecture, parallelism, debugging and language support compared on the same test suite.Cypress vs Selenium
ComparisonWhere each wins, migration cost in both directions, and which teams should not switch.TestRail alternatives
AlternativesTest management platforms compared on cost, integrations and reporting depth.Jira test management tools
ComparisonXray, Zephyr, QMetry and native options compared for teams already on Jira.Selenium vs Playwright
ComparisonArchitecture, browser coverage, languages, waiting, debugging and migration, with a clear pick for new suites.Puppeteer vs Playwright
ComparisonFor testing and for scraping: browsers, languages, CDP access, PDF and tracing, and when Puppeteer still wins.Selenium alternatives
AlternativesPlaywright, WebdriverIO, Cypress and Robot Framework, each matched to the reason teams leave Selenium.Bruno vs Postman
ComparisonGit-native collections, offline use, pricing, CI runners and what migrating a Postman collection really involves.Insomnia vs Postman
ComparisonAPI design versus API testing: storage, pricing, CI reporting and which one suits which team.Postman alternatives
AlternativesBruno, Insomnia, Hoppscotch, REST Assured, Karate and SoapUI compared on price, storage and CI, after Postman 12.Vitest vs Jest
ComparisonESM and TypeScript handling, Vite integration, mocking differences and a migration checklist, without invented benchmarks.Gradle vs Maven
ComparisonFor test automation teams: Surefire and Failsafe versus the Gradle test task, parallel tests, caching and CI.Jenkins vs GitHub Actions
ComparisonRunning cost, plugins versus actions, runners, test reporting, sharding, security and migration.GitLab vs GitHub
ComparisonThe platform decision for engineering and QA teams: hosting, pricing tiers, CI/CD, security scanning and test reports.Jenkins alternatives
AlternativesGitHub Actions, GitLab CI/CD, Azure Pipelines, CircleCI and TeamCity, and what each costs to run.k6 vs JMeter
ComparisonScripting model, protocols, distributed runs, CI thresholds, reporting and Grafana Cloud k6 pricing.LambdaTest vs BrowserStack
ComparisonLambdaTest, now TestMu AI, against BrowserStack on devices, parallel pricing, tunnels and test reporting.BrowserStack alternatives
AlternativesSauce Labs, TestMu AI, AWS Device Farm and self-hosted grids, with BrowserStack pricing explained.Want the suite built, not just the framework chosen?
QAble designs and builds automation suites with ISTQB-certified engineers. Start with a free QA audit of your product and current coverage.