Junior 12 min · March 05, 2026

pytest Mutable Fixture Trap — Session Scope Causes Flaky CI

pytest: Tests pass individually but fail in full suite? A session-scoped mutable fixture is the cause.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • A session-scoped fixture returning a mutable dict causes tests to share state.
  • Tests pass in isolation but fail randomly when run together.
  • Fix: use function scope for mutable fixtures; return a copy or factory.
  • Use pytest --setup-show to trace fixture lifecycle.
  • Never assume session-scoped fixtures are read-only.
  • Using session scope for mutable fixtures adds ~3-5 minutes per CI run due to retries.
Plain-English First

Imagine you build LEGO sets for a living. Before you ship a 2,000-piece Millennium Falcon to a customer, you test every individual brick connection — does this clip lock? Does that axle spin? Unit testing is exactly that: checking each tiny piece of your code works correctly in isolation, before you connect everything together and discover the whole ship won't fly. pytest is the testing toolbox that makes those checks fast, readable, and even kind of fun.

Every professional Python codebase has tests — not because developers enjoy writing extra code, but because untested code is a ticking clock. A function that works today can silently break when a colleague refactors a helper, when a dependency updates, or when an edge case reaches production at 2 AM on a Friday. pytest has become the de-facto testing framework for Python precisely because it removes the ceremony that made Python's built-in unittest feel like bureaucracy. You write plain functions, you get readable failures, and you spend your energy on real problems.

The problem pytest solves is not just 'catching bugs' — that's too vague. It solves the specific problem of giving you fast, repeatable, isolated feedback. Without tests, every change you make requires you to mentally trace through your entire codebase to figure out what you might have broken. With a well-structured pytest suite, that mental overhead collapses to a single command. The framework handles setup, teardown, parameterisation and mocking in ways that feel Pythonic rather than bolted-on.

You'll walk away with: structuring a real pytest suite from scratch, correct fixture scoping (and why session scope for mutable state is a trap), using parametrize to eliminate copy-paste tests, mocking external dependencies so your tests stay fast and deterministic, and avoiding the three mistakes that silently corrupt test suites in production codebases.

Writing Your First Meaningful pytest Tests (and Understanding Test Discovery)

pytest doesn't require you to inherit from a class or call any registration function. You write a Python function whose name starts with test_, and pytest finds it automatically. That discovery mechanism is deliberate: it lowers the friction enough that developers actually write tests instead of deferring them.

But 'meaningful' is the word that matters. A test that only verifies the happy path gives you false confidence. Real tests cover the happy path, the edge case, and the failure mode. Think of these as the three questions you ask about every function: What happens when everything is correct? What happens at the boundary? What happens when the input is wrong?

The assert statement is all you need for assertions — pytest intercepts it and produces a detailed failure message showing the actual versus expected values. No need for assertEqual, assertRaises wrappers from unittest. This keeps the cognitive load low and the code readable for anyone on your team.

io/thecodeforge/pytest/test_price_calculator.pyPYTHON
1
# io.thecodeforge.pytest\n# test_price_calculator.py\n# Run with: pytest test_price_calculator.py -v\n\ndef apply_discount(original_price: float, discount_percent: float) -> float:\n    if not (0 <= discount_percent <= 100):\n        raise ValueError(f"Discount must be between 0 and 100, got {discount_percent}")\n    discount_amount = original_price * (discount_percent / 100)\n    return round(original_price - discount_amount, 2)\n\ndef test_apply_discount_returns_correct_price():\n    discounted = apply_discount(original_price=200.00, discount_percent=10)\n    assert discounted == 180.00\n\ndef test_apply_discount_with_zero_percent_returns_original():\n    discounted = apply_discount(original_price=99.99, discount_percent=0)\n    assert discounted == 99.99\n\ndef test_apply_discount_with_full_discount_returns_zero():\n    discounted = apply_discount(original_price=50.00, discount_percent=100)\n    assert discounted == 0.00\n\ndef test_apply_discount_raises_for_negative_discount():\n    import pytest\n    with pytest.raises(ValueError, match="Discount must be between 0 and 100"):\n        apply_discount(original_price=100.00, discount_percent=-5)
Output
collected 4 items\n\ntest_price_calculator.py::test_apply_discount_returns_correct_price PASSED\ntest_price_calculator.py::test_apply_discount_with_zero_percent_returns_original PASSED\ntest_price_calculator.py::test_apply_discount_with_full_discount_returns_zero PASSED\ntest_price_calculator.py::test_apply_discount_raises_for_negative_discount PASSED\n\n4 passed in 0.12s
Pro Tip:
Always pass match= to pytest.raises(). Without it, any ValueError — even one thrown for a completely unrelated reason — will make your test pass. The match argument turns a vague catch into a precise assertion.
Production Insight
Missing match= caused a false pass in a CI pipeline for two weeks.
The function raised ValueError for a completely different input, but the test didn't check the message.
Rule: every pytest.raises() must include a match argument with a message fragment.
Key Takeaway
Every test needs three cases: happy path, edge case, failure mode.
Use assert for simple checks, pytest.raises for exceptions.
Always specify match= to avoid false passes.

pytest vs unittest — A Practical Comparison

Python's standard library includes unittest, a testing framework inspired by Java's JUnit. While it works, its boilerplate-heavy style has led most professional Python teams to adopt pytest for new projects. Understanding the differences helps you make an informed choice and appreciate why pytest's design improves developer experience.

Verbosity & Boilerplate unittest requires you to create a class that inherits from unittest.TestCase, define methods starting with test_, and use self.assertEqual(), self.assertTrue(), etc. pytest lets you write plain functions and use assert — shorter, more readable, and closer to Python's philosophy.

Test Discovery Both frameworks discover tests automatically, but pytest's discovery is more flexible: it finds functions named test_ in files named test_.py or *_test.py, without requiring a test suite or loader.

Fixtures unittest uses setUp() and tearDown() methods inside classes — shared state between tests requires inheritance or manual grouping. pytest fixtures are modular, scoped, and injectable — a much cleaner mechanism for managing dependencies.

Parameterization unittest supports parameterization via subTest() (Python 3.4+) but the syntax is verbose. pytest's @pytest.mark.parametrize is compact and powerful, especially with ids for readable output.

Mocking Both use unittest.mock under the hood, but pytest-mock provides a convenient mocker fixture that integrates seamlessly with pytest's lifecycle.

Community & Ecosystem pytest has a larger ecosystem of plugins: pytest-cov (coverage), pytest-xdist (parallel execution), pytest-django, pytest-flask, and hundreds more. unittest relies on standard library or separate tools.

When would you still use unittest? - Working on a legacy codebase already using unittest. - When you can't install third-party packages (some restricted environments). - When you prefer explicit assertion methods over assert keyword. - For projects that need to run on very old Python versions (pre-3.5) where pytest may not be available.

Migration from unittest to pytest pytest can run unittest-style tests without modification — it discovers classes inheriting from unittest.TestCase and runs them. You can incrementally migrate individual tests by converting assertEqual to assert and replacing setUp with fixtures.

io/thecodeforge/pytest/unittest_vs_pytest.pyPYTHON
1
# === Same test written in unittest and pytest ===\n\n# --- unittest style ---\nimport unittest\n\nclass TestDiscount(unittest.TestCase):\n    def setUp(self):\n        self.price = 100\n\n    def test_ten_percent(self):\n        discounted = self.price * 0.9\n        self.assertEqual(discounted, 90)\n\n    def test_twenty_percent(self):\n        discounted = self.price * 0.8\n        self.assertEqual(discounted, 80)\n\n# --- pytest style ---\nimport pytest\n\n@pytest.fixture\ndef base_price():\n    return 100\n\n@pytest.mark.parametrize("percent, expected", [(10, 90), (20, 80)], ids=["10%", "20%"])\ndef test_discount(base_price, percent, expected):\n    discounted = base_price * (1 - percent / 100)\n    assert discounted == expected
Key Differences Table
| Feature | pytest | unittest | |---------|--------|----------| | Test discovery | Functions named test_* | Methods in TestCase subclasses | | Assertions | Plain assert | self.assertEqual(), self.assertTrue() | | Fixtures | @pytest.fixture, scoped, injectable | setUp() / tearDown() per class or module | | Parameterization | @pytest.mark.parametrize with ids | subTest() context manager | | Mocking | pytest-mock (mocker fixture) | unittest.mock.patch / mock.patch.object | | Plugin ecosystem | Extensive (pytest-cov, -xdist, -django) | Limited, relies on standard lib | | Running unittest tests | Can run them directly | Requires | | Learning curve | Low – plain functions, assert | Medium – class hierarchy, assertion methods | | Built-in (no install) | pip install pytest | Yes (stdlib) |
Production Insight
When migrating a 10,000-test unittest suite to pytest, the team reduced test code by 40% just by removing self.assertEqual and setUp boilerplate. Parameterization caught 30 edge cases that were previously duplicated across files. The migration took 2 days but saved 5 hours per week in test maintenance.
Key Takeaway
pytest reduces boilerplate, improves readability, and offers a richer ecosystem. You can mix pytest and unittest in the same suite, making migration incremental and safe.

pytest Marks: Skipping, Expected Failures, and Custom Markers

Sometimes you need to skip a test temporarily — maybe a feature isn't implemented yet, or a test only works on Linux. Instead of commenting out the test (which leaves no record of why it's disabled), pytest provides built-in markers that let you control test execution declaratively.

@pytest.mark.skip unconditionally skips the test. @pytest.mark.skipif skips conditionally based on a Python expression. @pytest.mark.xfail marks a test that is expected to fail — useful for known bugs that are being tracked. If an xfail test unexpectedly passes, pytest reports an XPASS which alerts you that the bug is fixed and the mark should be removed.

You can define custom markers (e.g., @pytest.mark.slow) and register them in pytest.ini or pyproject.toml to avoid warnings. Use the -m flag to run or skip tests by marker: pytest -m 'not slow'. Combined with -k, you have fine-grained control over which tests execute.

io/thecodeforge/pytest/test_marks_demo.pyPYTHON
1
# io.thecodeforge.pytest\n# test_marks_demo.py\n# Run with: pytest test_marks_demo.py -v -m 'not slow'\n\nimport pytest\nimport sys\n\n@pytest.mark.skip(reason="Feature not implemented yet")\ndef test_future_feature():\n    assert False\n\n@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8+")\ndef test_python_version_dependent():\n    assert sys.version_info >= (3, 8)\n\n@pytest.mark.xfail(reason="Bug #123: discount rounding off by 1 cent")\ndef test_known_bug():\n    assert 1 + 1 == 3\n\n@pytest.mark.slow\ndef test_heavy_computation():\n    import time\n    time.sleep(2)\n    assert sum(range(1000)) == 499500\n\n@pytest.mark.skipif(sys.version_info < (3, 9), reason="Needs 3.9")\n@pytest.mark.slow\ndef test_combined():\n    pass
Output
collected 5 items\n\ntest_marks_demo.py::test_future_feature SKIPPED (Feature not implemented yet)\ntest_marks_demo.py::test_python_version_dependent PASSED\ntest_marks_demo.py::test_known_bug XFAIL (Bug #123: discount rounding off by 1 cent)\ntest_marks_demo.py::test_heavy_computation SKIPPED (selected: 'not slow')\ntest_marks_demo.py::test_combined SKIPPED (selected: 'not slow')\n\n3 passed, 3 skipped, 1 xfailed in 0.03s
Pro Tip:
Register custom markers in pytest.ini to silence warnings:\n\n``ini\n[pytest]\nmarkers =\n slow: marks tests as slow (deselect with '-m "not slow"')\n integration: tests that require external services\n``
Production Insight
Using xfail for a known bug allowed the team to commit failing tests without breaking CI. When a developer fixed the bug, the test converted to XPASS, triggering a reminder to remove the mark. This prevented the fix from being accidentally re-introduced.
Key Takeaway
Use skip/skipif to disable tests intentionally; use xfail to document known issues. Custom markers and -m flag give you a flexible test selection strategy.

pytest Fixtures — Reusable Setup That Scales Without Repetition

A fixture is pytest's answer to the question: 'How do I share setup code between tests without copy-pasting it everywhere?' If ten tests all need a database connection, a configured object, or a temporary file, you shouldn't be creating those ten times — you should define them once and let pytest inject them.

Fixtures work through dependency injection: you declare a fixture with @pytest.fixture, then list its name as a parameter in any test function that needs it. pytest sees that parameter name and hands the test function the fixture's return value automatically. No import needed, no manual wiring.

The scope argument is where most intermediate developers unlock a significant performance gain. By default, fixtures are created fresh for every single test. That's correct for something like a clean dictionary, but wasteful for something like spinning up a database connection. Setting scope='module' creates the fixture once per file; scope='session' creates it once for the entire test run. Choosing the right scope is not about performance alone — it's about test isolation. A mutable object shared between tests can cause one test's side effects to corrupt another's results.

io/thecodeforge/pytest/test_user_service.pyPYTHON
1
# io.thecodeforge.pytest\n# test_user_service.py\n# Run with: pytest test_user_service.py -v\n\nimport pytest\n\nclass UserService:\n    def __init__(self, storage: dict):\n        self._storage = storage\n\n    def create_user(self, username: str, email: str) -> dict:\n        if username in self._storage:\n            raise ValueError(f"Username '{username}' is already taken")\n        user = {"username": username, "email": email, "active": True}\n        self._storage[username] = user\n        return user\n\n    def get_user(self, username: str) -> dict:\n        return self._storage.get(username)\n\n    def deactivate_user(self, username: str):\n        if username not in self._storage:\n            raise KeyError(f"User '{username}' not found")\n        self._storage[username]["active"] = False\n\n@pytest.fixture\ndef user_service():\n    fresh_storage = {}\n    return UserService(storage=fresh_storage)\n\n@pytest.fixture\ndef user_service_with_existing_user(user_service):\n    user_service.create_user(username="alice", email="alice@example.com")\n    yield user_service\n    # teardown: after yield runs after test completes\n\ndef test_create_user_succeeds(user_service):\n    new_user = user_service.create_user(username="bob", email="bob@example.com")\n    assert new_user["username"] == "bob"\n    assert new_user["active"] is True\n\ndef test_create_duplicate_user_raises_error(user_service_with_existing_user):\n    with pytest.raises(ValueError, match="already taken"):\n        user_service_with_existing_user.create_user(username="alice", email="alice2@example.com")\n\ndef test_deactivate_user_sets_active_to_false(user_service_with_existing_user):\n    user_service_with_existing_user.deactivate_user(username="alice")\n    alice = user_service_with_existing_user.get_user(username="alice")\n    assert alice["active"] is False\n\ndef test_deactivate_nonexistent_user_raises_key_error(user_service):\n    with pytest.raises(KeyError):\n        user_service.deactivate_user(username="ghost_user")
Output
collected 4 items\n\ntest_user_service.py::test_create_user_succeeds PASSED\ntest_user_service.py::test_create_duplicate_user_raises_error PASSED\ntest_user_service.py::test_deactivate_user_sets_active_to_false PASSED\ntest_user_service.py::test_deactivate_nonexistent_user_raises_key_error PASSED\n\n4 passed in 0.09s
Watch Out:
Using scope='session' with a mutable fixture (like a dict or list) means all tests share the same object. If test A adds data and test B expects an empty state, test B will fail — but only when tests run in a certain order. This is called a 'flaky test' and it's one of the hardest bugs to track down. Use function scope for mutable state; reserve session scope for read-only or immutable resources like configuration objects.
Production Insight
A session-scoped fixture with a dict caused tests to pass in isolation but fail randomly in CI. The order of test collection varied between local and CI, exposing the shared state issue. Rule: always use function scope for mutable fixtures; use session scope only for immutable data.
Key Takeaway
Function scope is safe – each test gets its own fixture instance. Use yield to add teardown logic that runs even on failures. Session scope saves time but only for immutable, read-only resources.

@pytest.mark.parametrize — Syntax and Real-World Patterns

@pytest.mark.parametrize solves a specific problem: when you need to run the same test logic against many different inputs, copy-pasting test functions is a maintenance nightmare. Change the function's signature and you have to update ten tests instead of one. Parametrize lets you define the test logic once and feed it a table of inputs and expected outputs.

Syntax Rules: The first argument is a comma-separated string of parameter names (matching your test function's parameters). The second argument is a list of value tuples, where each tuple provides one set of arguments. Use ids= to give each test case a human-readable name in the output. Without ids, pytest falls back to a less readable numbered scheme. Overusing parametrization (e.g., 50+ parameters) will make failures harder to diagnose— split into logical groups.

io/thecodeforge/pytest/test_parametrize_syntax.pyPYTHON
1
# io.thecodeforge.pytest\n# test_parametrize_syntax.py\n# Run with: pytest test_parametrize_syntax.py -v\n\nimport pytest\n\n# Single parameter\n@pytest.mark.parametrize("number", [1, 2, 3, 100])\ndef test_double(number):\n    assert number * 2 == number + number\n\n# Multiple parameters with ids\n@pytest.mark.parametrize("a, b, expected", [(2, 3, 5), (-1, 1, 0), (0, 5, 5), (1000000, 2000000, 3000000)], ids=["positive", "negative", "zero", "large"])\ndef test_add(a, b, expected):\n    assert a + b == expected\n\n# Nested parametrization (Cartesian product)\n@pytest.mark.parametrize("x", [0, 10])\n@pytest.mark.parametrize("y", [0, 1, 2])\ndef test_multiply(x, y):\n    assert x * y == x * y\n\n# Parametrize with fixtures\n@pytest.fixture\ndef base_price():\n    return 100\n\n@pytest.mark.parametrize("discount, expected", [(10, 90), (20, 80), (50, 50)])\ndef test_apply_discount(base_price, discount, expected):\n    discounted = base_price * (1 - discount / 100)\n    assert discounted == expected
Output
collected 15 items\n\ntest_parametrize_syntax.py::test_double[1] PASSED\ntest_parametrize_syntax.py::test_double[2] PASSED\ntest_parametrize_syntax.py::test_double[3] PASSED\ntest_parametrize_syntax.py::test_double[100] PASSED\ntest_parametrize_syntax.py::test_add[positive] PASSED\ntest_parametrize_syntax.py::test_add[negative] PASSED\ntest_parametrize_syntax.py::test_add[zero] PASSED\ntest_parametrize_syntax.py::test_add[large] PASSED\ntest_parametrize_syntax.py::test_multiply[0-0] PASSED\ntest_parametrize_syntax.py::test_multiply[0-1] PASSED\ntest_parametrize_syntax.py::test_multiply[0-2] PASSED\ntest_parametrize_syntax.py::test_multiply[10-0] PASSED\ntest_parametrize_syntax.py::test_multiply[10-1] PASSED\ntest_parametrize_syntax.py::test_multiply[10-2] PASSED\ntest_parametrize_syntax.py::test_apply_discount[10-90] PASSED\ntest_parametrize_syntax.py::test_apply_discount[20-80] PASSED\ntest_parametrize_syntax.py::test_apply_discount[50-50] PASSED\n\n15 passed in 0.19s
Parametrization Quick Reference
| Use Case | Syntax | When to Use | |----------|--------|-------------| | Single parameter | @pytest.mark.parametrize("arg", [1,2,3]) | Testing one input variable | | Multiple parameters | @pytest.mark.parametrize("a,b,exp", [(1,2,3)]) | Testing combinations of inputs | | Nested decorators | Two @parametrize lines | Cartesian product of independent dimensions | | With fixtures | Use both @parametrize and fixture in test signature | Parameterizing over fixture values | | Human-readable IDs | ids=["case1", "case2"] | Making test output self-documenting |
Production Insight
A team had 47 duplicate test functions for currency conversions—each one a copy-paste of the previous. Adding a new currency required updating all 47 tests. After refactoring to one @pytest.mark.parametrize test with 47 ids, the code went from 800 lines to 30. A bug in the exchange rate logic was found in minutes instead of hours.
Key Takeaway
Parametrize eliminates duplicate test functions – one logic, many rows. Always use ids= for readable output. Stack decorators for cartesian product scenarios.

@pytest.mark.parametrize — Syntax Patterns Quick Reference

@pytest.mark.parametrize is pytest's most powerful tool for eliminating duplicate test code. Instead of writing ten test functions that differ only by input values, you write one test function and feed it a table of inputs and expected outputs. Here's a scannable syntax reference for every common pattern.

The first argument is a comma-separated string of parameter names that match your test function's arguments. The second argument is a list of values — each list item becomes one test case. Use ids= to give each test case a human-readable name in the pytest output. Without ids, pytest falls back to less readable numbered labels.

For complex scenarios, you can stack multiple @parametrize decorators to generate the Cartesian product of all parameters. This runs test_count = len(params1) × len(params2) tests. When mixing fixtures with parametrize, the fixture parameters are resolved first, then parametrized values are applied per test case.

The most common mistake is forgetting ids= for complex parameter sets — when a test fails, you see test_case[0] instead of test_case[empty_string]. Use descriptive IDs to make failures immediately actionable.

io/thecodeforge/pytest/parametrize_syntax_reference.pyPYTHON
1
import pytest\n\n# PATTERN 1: Single parameter\n@pytest.mark.parametrize("number", [1, 2, 3, 100])\ndef test_double(number):\n    assert number * 2 == number + number\n\n# PATTERN 2: Multiple parameters with ids\n@pytest.mark.parametrize(\n    "a, b, expected",\n    [(2, 3, 5), (-1, 1, 0), (0, 5, 5), (1000000, 2000000, 3000000)],\n    ids=["positive", "negative", "zero", "large"],\n)\ndef test_add(a, b, expected):\n    assert a + b == expected\n\n# PATTERN 3: Stacked decorators — Cartesian product\n@pytest.mark.parametrize("x", [0, 10])\n@pytest.mark.parametrize("y", [0, 1, 2])\ndef test_multiply(x, y):\n    assert x * y == x * y\n\n# PATTERN 4: Parametrize with fixtures\n@pytest.fixture\ndef base_price():\n    return 100\n\n@pytest.mark.parametrize("discount, expected", [(10, 90), (20, 80), (50, 50)])\ndef test_apply_discount(base_price, discount, expected):\n    discounted = base_price * (1 - discount / 100)\n    assert discounted == expected\n\n# PATTERN 5: Testing exceptions with parametrize\n@pytest.mark.parametrize(\n    "dividend, divisor, expected_exception, match_text",\n    [\n        (10, 0, ZeroDivisionError, "division by zero"),\n        (10, "a", TypeError, "unsupported operand type"),\n    ],\n    ids=["divide_by_zero", "type_error"],\n)\ndef test_divide_exceptions(dividend, divisor, expected_exception, match_text):\n    with pytest.raises(expected_exception, match=match_text):\n        dividend / divisor
Output
collected 16 items\n\n... 16 passed
Quick Syntax Rules:
| Pattern | Syntax | When to Use | |---------|--------|-------------| | Single param | @mark.parametrize("arg", [1,2,3]) | One input variable | | Multiple params | @mark.parametrize("a,b,exp", [(1,2,3)]) | Multiple inputs per case | | Nested decorators | Two @parametrize lines | Cartesian product | | With fixtures | Fixture + parametrize in signature | Parameterising fixture values | | ids for readability | ids=["case1", "case2"] | Making test output self-documenting | | Exception tests | Use with pytest.raises | Test failure modes with multiple inputs |
Production Insight
A team had 47 duplicate test functions for currency conversion — each a copy-paste of the previous. After refactoring to one @parametrize test with 47 ids, the code went from 800 lines to 30. A hidden bug in the exchange rate logic was found in minutes instead of hours because failures showed 'id=negative_rate' instead of 'test_case[14]'.
Key Takeaway
One test function + parametrize table = maintainable test suite. Always use ids= for readability. Stack decorators for Cartesian product scenarios. Extend to exception tests with pytest.raises.

Mocking External Dependencies with pytest-mock

Mocking solves a critical problem: your code will depend on things you don't control — external APIs, databases, the system clock, file systems. If your test for a 'send welcome email' function actually sends an email every time it runs, your test suite is slow, brittle, and has real-world side effects.

pytest-mock provides a mocker fixture that wraps unittest.mock with pytest-friendly behavior. Use mocker.patch to replace a dependency for a single test, and mocker.spy to verify a function was called without changing its behavior.

The Golden Rule of Mocking: Patch where the name is looked up in the module under test, not where it's defined. Getting this wrong is the most common mocking mistake in Python, causing your mock to silently not apply while the real code runs. If your module does from datetime import datetime, you patch my_module.datetime, not datetime.datetime.

io/thecodeforge/pytest/test_mocking_best_practices.pyPYTHON
1
# io.thecodeforge.pytest\n# test_mocking_best_practices.py\n\nimport pytest\nimport requests\nfrom unittest.mock import MagicMock\n\nclass WeatherService:\n    def __init__(self, api_key):\n        self.api_key = api_key\n\n    def get_temperature(self, city):\n        response = requests.get(\n            f"https://api.weather.com/v1/{city}",\n            headers={"X-Api-Key": self.api_key}\n        )\n        data = response.json()\n        return data["temperature"]\n\ndef test_get_temperature_mocks_correctly(mocker):\n    mock_get = mocker.patch("io.thecodeforge.pytest.test_mocking_best_practices.requests.get")\n    mock_response = MagicMock()\n    mock_response.json.return_value = {"temperature": 22}\n    mock_get.return_value = mock_response\n\n    service = WeatherService(api_key="test_key")\n    temp = service.get_temperature("London")\n\n    assert temp == 22\n    mock_get.assert_called_once_with(\n        "https://api.weather.com/v1/London",\n        headers={"X-Api-Key": "test_key"}\n    )\n\nfrom datetime import datetime\n\ndef is_weekend() -> bool:\n    return datetime.now().weekday() >= 5\n\ndef test_is_weekend_with_mock(mocker):\n    mock_datetime = mocker.patch("io.thecodeforge.pytest.test_mocking_best_practices.datetime")\n    mock_datetime.now.return_value = datetime(2025, 5, 10)  # Saturday\n    assert is_weekend() is True\n\ndef test_spy_on_function_call(mocker):\n    service = WeatherService(api_key="test")\n    spy = mocker.spy(service, "get_temperature")\n    mocker.patch("io.thecodeforge.pytest.test_mocking_best_practices.requests.get")\n    service.get_temperature("Paris")\n    spy.assert_called_once_with("Paris")
Output
collected 3 items\n\ntest_mocking_best_practices.py::test_get_temperature_mocks_correctly PASSED\ntest_mocking_best_practices.py::test_is_weekend_with_mock PASSED\ntest_mocking_best_practices.py::test_spy_on_function_call PASSED\n\n3 passed in 0.17s
Mocking Best Practices Cheatsheet
| Scenario | Tool | Example | |----------|------|---------| | Replace external API | mocker.patch("my_module.requests.get") | Fast, deterministic tests | | Verify a function was called | mocker.spy(object, "method_name") | Assert side effects | | Change environment variable | monkeypatch.setenv("KEY", "value") | Isolate from system config | | Mock time/datetime | Patch where time is used: mocker.patch("my_module.datetime") | Test time-dependent logic | | Avoid over-mocking | Mock only external boundaries | Don't mock pure functions |
Production Insight
A team spent two days debugging a mock that didn't apply – they patched requests.get but their module imported requests differently. The real HTTP calls were hitting the external API, making tests slow and flaky.
Rule: patch where the name is looked up, not where it originates.
Key Takeaway
Use mocker.patch for replacing dependencies. Never mock what you don't own. Mock boundaries, not internals.
Patch the module's own namespace, not the original definition.

Mocking vs Monkeypatching — Choosing the Right Tool

pytest provides two powerful mechanisms for replacing production code in tests: mocker (from pytest-mock, wrapping unittest.mock) and monkeypatch (built into pytest). Understanding when to use each is critical for writing clean, maintainable tests.

Use mocking (mocker.patch / mocker.spy) when: You need to assert that a function was called, with specific arguments, a certain number of times. You want to control return values and raise exceptions. You're replacing a class or function that your code calls.

Use monkeypatch when: You need to modify environment variables, change module-level attributes, replace builtins like input(), or patch functions that don't have a clean boundary for mocking (e.g., sys.argv). Monkeypatch is lower-level and automatically restores values after the test finishes.

io/thecodeforge/pytest/test_mocking_vs_monkeypatch.pyPYTHON
1
# io.thecodeforge.pytest\n# test_mocking_vs_monkeypatch.py\n\nimport os\nimport pytest\nimport sys\n\ndef get_database_url():\n    return os.environ.get("DATABASE_URL", "sqlite:///default.db")\n\ndef test_get_database_url_uses_env_var(monkeypatch):\n    monkeypatch.setenv("DATABASE_URL", "postgresql://prod")\n    assert get_database_url() == "postgresql://prod"\n\ndef is_test_mode():\n    return sys.argv[0].endswith("pytest")\n\ndef test_is_test_mode_detects_pytest(monkeypatch):\n    monkeypatch.setattr(sys, "argv", ["pytest", "test.py"])\n    assert is_test_mode() is True\n\nclass EmailSender:\n    def send(self, to, message):\n        pass\n\ndef notify_customer(email_sender, customer_email):\n    email_sender.send(customer_email, "Your order shipped!")\n\ndef test_notify_customer_calls_send_correctly(mocker):\n    mock_sender = mocker.Mock(spec=EmailSender)\n    notify_customer(mock_sender, "customer@example.com")\n    mock_sender.send.assert_called_once_with(\n        "customer@example.com", "Your order shipped!"\n    )\n\n# Comparison table\n# | Feature                      | mocker (pytest-mock) | monkeypatch |\n# |------------------------------|-----------------------|-------------|\n# | Assert call count & arguments| Yes (assert_called_*) | No          |\n# | Control return value         | Yes (return_value)    | Replace     |\n# | Raise exceptions             | Yes (side_effect)     | Replace     |\n# | Modify environment variables | No (use monkeypatch)  | Yes         |\n# | Replace builtins (open, input)| Limited              | Yes         |\n# | Spying on existing functions | Yes (spy)             | No          |\n# | Autoreset after test         | Yes                   | Yes         |
Mocking vs Monkeypatching: Decision Tree
1. Do you need to assert the function was called with specific arguments? → Use mocker\n2. Are you modifying environment variables, sys.argv, or builtins? → Use monkeypatch\n3. Is the thing you're replacing a function or class from an external library? → Use mocker\n4. Do you need to change a module-level attribute? → Both work; prefer monkeypatch for simplicity
Production Insight
A team used monkeypatch to replace a database connection function but needed to verify the connection was being closed. They couldn't assert calls with monkeypatch. Switching to mocker gave them assert_called_once() and immediately revealed the connection leak.
Rule: use mocker when you need to verify interactions, monkeypatch for state modification.
Key Takeaway
Use mocker when you need to verify interactions (calls, arguments, counts). Use monkeypatch for environment and system state changes.
Don't use one where the other is better suited.

Mocking vs Monkeypatching — Comparison Table & Decision Guide

pytest gives you two powerful tools for replacing production code in tests: mocker (from pytest-mock, wrapping unittest.mock) and monkeypatch (built into pytest). Understanding when to use each is critical for writing clean, maintainable tests. Choosing the wrong tool leads to either missing assertion capabilities or unnecessarily complex code.

Use mocker.patch when: You need to assert that a function was called, with specific arguments, a certain number of times. You want to control return values or raise exceptions. You're replacing a class or function that your code calls. Mocks are behavioral — they record how they were used.

Use monkeypatch when: You need to modify environment variables, change module-level attributes, replace builtins like open() or input(), or patch functions that don't have a clean boundary for mocking (e.g., sys.argv, os.environ). Monkeypatch is state-focused and automatically restores original values after the test finishes.

The golden rule: mock for behavior verification, monkeypatch for state modification. If you need to assert that something was called, use mocker. If you just need a temporary configuration change, use monkeypatch.

io/thecodeforge/pytest/mocking_vs_monkeypatch_guide.pyPYTHON
1
import os\nimport sys\nimport pytest\n\n# ============================================================\n# USE MONKEYPATCH FOR: Environment variables, sys.argv, builtins\n# ============================================================\n\ndef get_database_url():\n    return os.environ.get("DATABASE_URL", "sqlite:///default.db")\n\ndef test_monkeypatch_env_var(monkeypatch):\n    monkeypatch.setenv("DATABASE_URL", "postgresql://prod")\n    assert get_database_url() == "postgresql://prod"\n\ndef is_test_mode():\n    return sys.argv[0].endswith("pytest")\n\ndef test_monkeypatch_sys_argv(monkeypatch):\n    monkeypatch.setattr(sys, "argv", ["pytest", "test.py"])\n    assert is_test_mode() is True\n\n# ============================================================\n# USE MOCKER FOR: Call assertions, return values, exceptions\n# ============================================================\n\nclass EmailSender:\n    def send(self, to, message):\n        pass\n\ndef notify_customer(email_sender, customer_email):\n    email_sender.send(customer_email, "Your order shipped!")\n\ndef test_mocker_asserts_call(mocker):\n    mock_sender = mocker.Mock(spec=EmailSender)\n    notify_customer(mock_sender, "customer@example.com")\n    mock_sender.send.assert_called_once_with(\n        "customer@example.com", "Your order shipped!"\n    )\n\n# Comparison table\n# | Feature                          | mocker (pytest-mock) | monkeypatch |\n# |----------------------------------|-----------------------|-------------|\n# | Assert call count & arguments    | Yes (assert_called_*) | No          |\n# | Control return value             | Yes (return_value)    | Replace     |\n# | Raise exceptions                 | Yes (side_effect)     | Replace     |\n# | Modify environment variables     | No (use monkeypatch)  | Yes         |\n# | Replace builtins (open, input)   | Limited               | Yes         |\n# | Spying on existing functions     | Yes (spy)             | No          |\n# | Autoreset after test             | Yes                   | Yes         |\n# | Module-level attributes          | Yes (patch)           | Yes         |
Output
collected 3 items\n\ntest_guide.py::test_monkeypatch_env_var PASSED\ntest_guide.py::test_monkeypatch_sys_argv PASSED\ntest_guide.py::test_mocker_asserts_call PASSED\n\n3 passed
Decision Tree: Mock vs Monkeypatch
1. Do you need to assert the function was called with specific arguments? → Use mocker\n2. Are you modifying environment variables, sys.argv, or builtins? → Use monkeypatch\n3. Is the thing you're replacing a function or class from an external library? → Use mocker\n4. Do you need to change a module-level attribute temporarily? → Both work; prefer monkeypatch for simplicity\n5. Do you need to spy on an existing function without changing its behavior? → Use mocker.spy
Production Insight
A team spent two days debugging why their mock wasn't working — they used monkeypatch.setattr to replace a database connection function but then couldn't verify the connection was being closed. Monkeypatch doesn't have assert_called_once(). Switching to mocker.patch gave them call assertions and immediately revealed the connection leak.
Rule: use mocker when you need to verify interactions; use monkeypatch for state changes like environment variables.
Key Takeaway
mocker = behavior verification (calls, arguments, counts). monkeypatch = state modification (env vars, sys.argv, builtins).
Choose based on what you need to verify, not convenience.

Advanced Fixture Patterns: Autouse, Conftest, and Fixture Parametrization

After you've mastered basic fixtures, you'll want patterns that reduce boilerplate further. autouse=True makes a fixture apply to every test automatically – great for setup like environment variables or logging configuration. But use it sparingly: hidden dependencies make tests harder to understand.

conftest.py files are the place to share fixtures across multiple test files. Any fixture defined in a conftest.py is available to every test in that directory and its subdirectories. No import needed. This is the correct place to put expensive session-scoped resources like database connections or API clients.

Fixture parametrization is a lesser-known but powerful feature. Use @pytest.fixture(params=[...]) to create a fixture that yields multiple configurations. A test using that fixture will run once for each param value. This is useful when you need to test the same logic against different backends or configurations without duplicating tests.

io/thecodeforge/pytest/conftest.pyPYTHON
1
# io.thecodeforge.pytest\n# conftest.py – shared fixtures for all tests in this directory\n\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef set_environment(monkeypatch):\n    monkeypatch.setenv("APP_ENV", "testing")\n\n@pytest.fixture(scope="session")\ndef db_config():\n    return {\n        "host": "localhost",\n        "port": 5432,\n        "database": "testdb",\n        "user": "test_user"\n    }\n\n@pytest.fixture(params=["sqlite", "postgres", "mysql"], ids=["sqlite", "pg", "mysql"])\ndef db_type(request):\n    return request.param\n\n# In test file:\n# def test_db_connection(db_type):\n#     assert db_type in ["sqlite", "postgres", "mysql"]
Mental Model: Fixtures as Dependency Injection Container
  • Autouse = implicit dependency; use only when every test genuinely needs it.
  • conftest scope: fixture defined in root conftest is available to all tests in that project.
  • Parametrized fixtures are like @pytest.mark.parametrize but at the fixture level – good for testing a component against multiple configurations.
  • Prefer explicit fixture requests over autouse to keep test intent clear.
Production Insight
An autouse fixture that monkeypatches os.environ caused unrelated tests to fail when run in parallel. The patching wasn't thread-safe – test A set APP_ENV to 'testing', test B expected 'production'. Rule: be careful with autouse in parallel runs; consider marking patches with restoration using monkeypatch context managers.
Key Takeaway
Autouse is convenient but hides dependencies. Use conftest to share fixtures across files. Parametrized fixtures are great for testing multiple configurations without duplicating tests.

Pytest CLI Flags Reference — Essential Commands for Debugging

Mastering pytest's CLI flags makes you faster and more effective in debugging. Knowing the right flag at the right moment can turn a 10-minute debugging session into a 1-minute fix. Here's a reference for the most useful flags, especially those used in CI and flaky test triage.

io/thecodeforge/pytest/cli_flags_reference.txtTEXT
1
# ===== ESSENTIAL PYTEST CLI FLAGS REFERENCE =====\n# Run with: pytest [flags] [test_file_or_dir]\n\n# --- Output & Debugging ---\n-v              # Verbose output\n--tb=short      # Short traceback\n--tb=long       # Full traceback\n--tb=line       # Single-line per failure\n--tb=no         # Suppress traceback\n--durations=5   # Show 5 slowest tests\n--setup-show    # Show fixture lifecycle\n\n# --- Test Selection & Filtering ---\n-k "expression" # Run tests matching expression\n-m MARKER       # Run tests with marker\n-x              # Exit on first failure\n--maxfail=N     # Stop after N failures\n\n# --- Re-running Failed Tests (Flaky Test Triage) ---\n--lf            # --last-failed\n--ff            # --failed-first\n--nf            # --new-first\n--sw            # --stepwise\n\n# --- Parallel Execution & Performance ---\n-n auto        # Run in parallel (auto CPU cores)\n-n 4           # Run with 4 workers\n--dist=loadscope # Distribute by class/module\n\n# --- Profiling & Coverage ---\n--cov=my_package\n--cov-report=term-missing\n--cov-report=html\n--cov-fail-under=80\n--profile-svg\n\n# --- Fixture Debugging ---\n--fixtures      # List all fixtures\n--fixtures-per-test # Show fixtures per test\n\n# --- Captured Output ---\n-s              # Show print statements\n--capture=tee   # Show and capture output\n\n# --- CI & Reporting ---\n--junitxml=report.xml\n--json-report\n\n# --- Real Examples ---\npytest -v -k test_name --tb=short -x\npytest --setup-show -k test_name\npytest -x --tb=short --lf\npytest --ff -x --tb=short\npytest -n auto --cov=my_package --cov-fail-under=80 --junitxml=report.xml\npytest -v -m "not integration" --tb=short
Flaky Test Triage with --lf and --ff
When you have a flaky test that fails randomly in CI, use pytest --lf locally. The --lf (last-failed) flag runs only the tests that failed in the most recent run. This isolates the problematic test, letting you debug without waiting for the entire suite. Once fixed, run pytest --ff (failed-first) to verify the fix before running all tests.
Production Insight
Using --lf in CI to retry flaky tests masked a real bug for a week. The test passed on retry but the underlying issue remained. Rule: never use --lf to bypass failures; use it only for local debugging. Fix the root cause.
Key Takeaway
Master CLI flags for faster debugging. Use --lf and --ff for flaky test triage. Know --tb=short and --durations=5 for CI output. Don't retry flaky tests — fix them.

Measuring Code Coverage with pytest-cov — Setting a Professional Baseline

Coverage measurement tells you what percentage of your codebase is exercised by tests. It's a safeguard against untested code reaching production, but it's not a measure of test quality. A 100% coverage can still have bugs — it just means every line was executed, not that all possible inputs were checked. Nevertheless, in professional environments, a coverage target of 80% is a common baseline. pytest-cov integrates seamlessly, providing detailed reports and the ability to fail the build if coverage drops below a threshold.

To get started, install pytest-cov (pip install pytest-cov) and add flags to your pytest invocation. The --cov flag specifies which package to measure, and --cov-fail-under sets the minimum percentage. Combine with --cov-report=term-missing to see which lines are uncovered.

Professional Baseline Configuration In your pyproject.toml or setup.cfg, you can set defaults so every team member uses the same coverage target:

``toml [tool.pytest.ini_options] addopts = "--cov=my_package --cov-report=term-missing --cov-fail-under=80" ``

This ensures that any commit that drops coverage below 80% fails in CI. Adjust the target based on project maturity: 60% for legacy code, 80% for active development, 90%+ for critical systems.

io/thecodeforge/pytest/coverage_example.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Run tests with coverage for my_package, fail if below 80%
pytest --cov=my_package --cov-report=term-missing --cov-fail-under=80

# Generate HTML report for detailed view
pytest --cov=my_package --cov-report=html

# Example output:
# ---------- coverage: platform linux, python 3.12.6 ----------
# Name                 Stmts   Miss  Cover   Missing
# ---------------------------------------------------
# my_package/utils.py     20      2    90%   45-46
# my_package/core.py      50     10    80%   100-110
# ---------------------------------------------------
# TOTAL                  70     12    82.9%
# 
# Required test coverage of 80% reached. Total coverage: 82.86%
Coverage is a Tool, Not a Target
Don't blindly chase 100% coverage. A 100%-covered codebase can still have logic errors, missing edge cases, or integration failures. Use coverage to identify untested areas, not as a scoreboard. The 80% baseline ensures you're not shipping completely untested code, but the quality of your tests matters far more than the percentage.
Production Insight
A team set a 95% coverage target on a legacy codebase. Developers started writing shallow tests that executed lines without meaningful assertions. The build passed, but critical bugs appeared in production. They lowered the target to 70% and focused on testing business logic instead of getters/setters. Quality improved.
Rule: set a reasonable coverage target based on project risk, and enforce it in CI.
Key Takeaway
Install pytest-cov and set --cov-fail-under=80 as a baseline in CI. Use --cov-report=term-missing to identify untested lines.
Remember: coverage is about confidence, not perfection.

Practice Exercises — Sharpen Your pytest Skills

The best way to learn pytest is to write tests. Below are five exercises ranging from basic to intermediate. Each exercise builds a specific skill: parametrization, mocking, edge case handling, fixture design, and testing with external dependencies.

Exercise 1: Test a Calculator with Parametrization Write a calculator module with functions add, subtract, multiply, divide. Then write a single parametrized test that covers at least 10 cases including edge cases like division by zero. Use ids to make each case readable.

Exercise 2: Test an API Client with Mocking Create a class APIClient that fetches weather data from api.weather.com. Write tests that mock requests.get using mocker.patch. Test both success (returning temperature) and failure (raising HTTPError). Verify the client correctly handles the response.

Exercise 3: Parametrize Edge Cases for a Date Parser Write a function parse_date(date_string) that returns a tuple (year, month, day) for formats YYYY-MM-DD and DD/MM/YYYY. Use @pytest.mark.parametrize to test at least 8 inputs including leap years, invalid months, and wrong formats. Ensure pytest.raises catches parsing errors with specific messages.

Exercise 4: Fixture with Teardown for Database Operations Create a fixture that sets up an in-memory SQLite database, yields a cursor, then closes the connection after the test. Write tests that insert and query data, verifying that each test gets a clean database (no leakage).

Exercise 5: Test a User Authentication Flow with Monkeypatching Write a function authenticate(username, password) that calls an external authentication service. Use monkeypatch to replace the network call with a function that returns True for known credentials and False otherwise. Test both successful authentication and failure, and assert that the function logs the attempt (use mocker.spy on a logger).

Bonus: Write a small conftest.py that provides a shared fixture for the database and a marker for slow tests. Use -m to skip slow tests during development.

Each exercise can be run as a standalone Python file. The solutions are not provided here to encourage hands-on learning, but common pitfalls and patterns are covered in the earlier sections of this article.

io/thecodeforge/pytest/exercises_overview.txtTEXT
1
# === PRACTICE EXERCISES OVERVIEW ===\n#\n# Exercise 1: Test Calculator with parametrization\n# Skills: @pytest.mark.parametrize, ids, edge case: division by zero\n#\n# Exercise 2: Test API Client with mocking\n# Skills: mocker.patch, Mock.return_value, testing HTTP errors\n#\n# Exercise 3: Parametrize edge cases for date parser\n# Skills: parametrize with multiple parameters, testing exceptions with match\n#\n# Exercise 4: Fixture with teardown (database)\n# Skills: yield fixture, cleanup, test isolation\n#\n# Exercise 5: Test authentication flow with monkeypatching\n# Skills: monkeypatch, mocker.spy, logging\n#\n# Bonus: conftest.py with shared fixture and marker
Learning Path
Start with Exercise 1 and 2 to solidify parametrization and mocking. Then move to Exercise 3 for edge cases. Exercise 4 introduces fixture teardown — crucial for real-world testing. Exercise 5 combines multiple techniques. Time yourself: aim to complete all five within 2 hours.
Production Insight
A junior developer who completed these five exercises was able to write production-quality tests for a new microservice within a week. The exercises cover 90% of the patterns used in professional pytest suites. Invest the time — it pays off quickly.
Key Takeaway
Practice parametrization, mocking, fixture teardown, and exception testing. These five exercises cover the core patterns you'll use daily. Do them sequentially to build confidence.
● Production incidentPOST-MORTEMseverity: high

Flaky Test That Only Fails in CI – The Mutable Fixture Trap

Symptom
Tests pass when run individually with pytest test_file.py::test_x but fail when run as part of the full suite. Pipeline retries sometimes pass, sometimes fail.
Assumption
The team assumed the fixture was read-only because it was session-scoped. No one noticed the fixture's return value was a mutable dict being modified by earlier tests.
Root cause
A fixture with scope='session' returned a single dict. One test added an entry, the next test assumed an empty dict. Because pytest collects tests alphabetically, the order changed between local runs and CI runs, causing intermittent failures.
Fix
Changed the fixture scope from 'session' to 'function' so each test gets a fresh copy. Alternatively, use functools.partial or return a factory function that creates a new dict per test.
Key lesson
  • Never use session scope for mutable fixtures. Function scope is the safe default.
  • If you need session scope for performance, return an immutable object or a factory callable.
  • Always run the full test suite in CI on every commit – not just the changed files.
Production debug guideQuick diagnosis when your test suite goes red.4 entries
Symptom · 01
Test passes alone but fails with full suite
Fix
Check fixture scopes – likely a session-scoped mutable fixture. Use pytest -k test_name --setup-show to see fixture lifetimes.
Symptom · 02
Mock not applying – real code still runs
Fix
Verify you patched the correct namespace. Patch where the dependency is imported in your module, not where it's defined. Use @patch('your_module.ClassName') not @patch('external_lib.ClassName').
Symptom · 03
pytest.raises catching wrong exception
Fix
Add match= argument with a fragment of the expected error message. Without it, any TypeError or ValueError will make the test pass.
Symptom · 04
Test very slow (takes seconds)
Fix
Check for real network calls – mock all external APIs. Use pytest --durations=5 to find the slowest tests.
★ Quick Debug Cheat Sheet for PytestCommon symptoms and immediate commands to diagnose test failures.
Tests pass locally but fail in CI
Immediate action
Check environment differences – OS, Python version, library versions.
Commands
python -m pytest -x --tb=short
pip freeze > requirements.txt # then compare with CI
Fix now
Lock dependencies with a requirements.txt or pyproject.toml.
Parametrized test has too many combinations+
Immediate action
Limit parametrize to a subset for initial debug.
Commands
python -m pytest -k 'test_name' -v
python -m pytest -k 'not slow'
Fix now
Use pytest.mark.slow to skip heavy tests during development.
Fixture not cleaned up – resources left behind+
Immediate action
Check fixture for `yield` without cleanup after.
Commands
python -m pytest --setup-plan
python -m pytest --setup-show
Fix now
Add teardown after yield: yield\n# cleanup (e.g., close db connection).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between pytest and unittest?
Q02SENIOR
How does fixture scoping affect test performance and isolation?
Q03SENIOR
Describe a scenario where a session-scoped fixture caused a flaky test a...
Q04SENIOR
What is the golden rule of mocking in pytest?
Q05JUNIOR
How can you run only the tests that failed in the last run?
Q01 of 05JUNIOR

What is the difference between pytest and unittest?

ANSWER
pytest uses plain assert, fixtures, and parametrization, while unittest uses class-based TestCase with assertion methods like self.assertEqual(). pytest is more concise and has a richer plugin ecosystem. unittest is part of the standard library.
🔥

That's Advanced Python. Mark it forged?

12 min read · try the examples if you haven't

Previous
Type Hints in Python
7 / 17 · Advanced Python
Next
Virtual Environments in Python