Skip to content

Why Choose pytest over unittest?

  • class-based (unittest.TestCase)
  • assertion methods (self.assertEqual)
  • more boilerplate
  • simple functions
  • plain assert
  • powerful fixtures
  • rich plugin ecosystem
test_add_unittest.py
import unittest
 
 
def add(a, b):
    return a + b
 
 
class TestAdd(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)
test_add_pytest.py
 
def add(a, b):
    return a + b
 
 
def test_add():
    assert add(2, 3) == 5
  • less ceremony
  • clearer failures
  • fixtures scale better than setUp/tearDown
  • easy parameterization
unittest
import unittest
from shop import price_with_tax
 
class TestPricing(unittest.TestCase):
    def test_tax(self):
        self.assertEqual(price_with_tax(100), 120.0)
pytest
from shop import price_with_tax
 
def test_tax():
    assert price_with_tax(100) == 120.0

No class, no self, no assertion vocabulary. What makes that possible is assertion rewriting — pytest rewrites the bytecode of assert statements at import time so it can report the operands:

measured pytest failure
>       assert price_with_tax(100) == 121.0
E       assert 120.0 == 121.0
E        +  where 120.0 = price_with_tax(100)

It reported the comparison and the sub-expression that produced the left-hand side. The same plain assert under unittest measured only AssertionError, with no values at all.

diagram Diagram mermaid

pytest runs unittest.TestCase classes unchanged. Adopting it does not mean rewriting anything — you install it, run pytest, and your existing suite executes with better failure output. New tests can then be written as plain functions.

unittestpytest
in the standard libraryyesno, pip install pytest
plain assert reports valuesnoyes
many inputssubTest@parametrize, each case its own test
shared setupsetUp per classfixtures, requested by name and scoped
selecting testsby name-k expressions and -m markers
pluginsfewa large ecosystem: coverage, HTML reports, xdist
runs the other’s testsnoyes

Measured on a small suite: five test functions became 8 tests, because one was parametrized with four cases and each case is reported separately.

  • A third-party dependency is genuinely unacceptable — pytest is not in the standard library.
  • The codebase is large, stable, and nobody is complaining about the tests.

Otherwise the cost is one install and the benefit starts with the first failure you have to diagnose.

sketch What the failure tells you p5.js
pytest rewrites assert statements so a plain assert reports both operands. unittest needs a specific assertion method to say anything useful.
pch.quizTag pch.quizDefaultTitle
  1. How can pytest report operand values from a plain assert when unittest cannot?

    pch.quizShowAnswer

    B — it rewrites the bytecode of assert statements when it imports your test module — Measured pytest printing assert 120.0 == 121.0 plus where 120.0 = price_with_tax(100). The same plain assert under unittest printed only AssertionError.

  2. What does adopting pytest require you to do with an existing unittest suite?

    pch.quizShowAnswer

    B — nothing; pytest runs unittest.TestCase classes unchanged — Install it, run pytest, and the existing suite executes with better failure output. New tests can be plain functions from then on.

  3. Five test functions produced 8 tests in the measured run. Why?

    pch.quizShowAnswer

    B — one function was parametrized with four cases, and each case is a separate test — parametrize expands one function into independently named and reported tests, which is what makes a failing case identifiable.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading