Typorithm · medium

Yield fixtures and reliable cleanup

pythonpytesttestingindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Yield fixtures and reliable cleanup

0%

Concept and contract

Own a resource, expose it for a test, and release it even when that test fails.

A resource fixture has a lifetime: acquire, use, release. A connection, temporary directory or patched environment should not remain active just because an assertion failed. A yield fixture places the test’s usable value at the yield, with cleanup code following it.

This lifetime applies after setup successfully reaches yield. If acquisition fails earlier, code after yield is not a universal cleanup guarantee. Pair each acquired resource with cleanup deliberately, using a context manager or try/finally around the owned value where appropriate.

Mechanism and worked example

The worked example uses a synthetic Resource with explicit close state.

The worked example uses a synthetic Resource with explicit close state. The fixture constructs it, yields it and closes it in finally. The test checks the resource is usable while the test runs. A separate unit test verifies that close changes the resource’s public state.

The playground’s lifecycle panel separates setup, call and teardown reports. A green call with a red teardown still means the test session failed. This helps explain why a cleanup error can coexist with a successful assertion rather than being dismissed as unrelated terminal noise.

Complete worked example

python

import pytest


class Resource:
    def __init__(self):
        self.closed = False

    def close(self):
        self.closed = True


@pytest.fixture
def resource():
    value = Resource()
    try:
        yield value
    finally:
        value.close()


def test_resource_is_open_during_call(resource):
    assert resource.closed is False


def test_close_releases_resource():
    value = Resource()
    value.close()
    assert value.closed is True
Reasoning
The worked example uses a synthetic Resource with explicit close state. The fixture constructs it, yields it and closes it in finally. The test checks the resource is usable while the test runs. A separate unit test verifies that close changes the resource’s public state. The playground’s lifecycle panel separates setup, call and teardown reports. A green call with a red teardown still means the test session failed. This helps explain why a cleanup error can coexist with a successful assertion rather than being dismissed as unrelated terminal noise.
Open in local Python playground ↗

Focused test decision: context is in the complete example

python

try:
    yield value
finally:
    value.close()
Focused test decision
Type this short test decision after understanding the complete runnable module. Desktop typing records practice; Run tests in the local playground executes real pytest.

Counterexamples and common mistakes

The controlled close method leaves the resource open.

The controlled close method leaves the resource open. Its direct unit test fails. If close were never asserted and the application had no observable consequence, merely running cleanup code would not prove that cleanup worked. Test the responsibility at a meaningful boundary.

Do not acquire several unrelated resources before installing any cleanup. If the second acquisition raises, the first may leak. Prefer small composable fixtures or an ExitStack when acquisition is dynamic. Do not let an assertion in the cleanup obscure an earlier failure without a clear diagnostic.

Controlled bug: the tests must report failure

python

import pytest


class Resource:
    def __init__(self):
        self.closed = False

    def close(self):
        self.closed = False


@pytest.fixture
def resource():
    value = Resource()
    try:
        yield value
    finally:
        value.close()


def test_resource_is_open_during_call(resource):
    assert resource.closed is False


def test_close_releases_resource():
    value = Resource()
    value.close()
    assert value.closed is True
Reasoning
The controlled close method leaves the resource open. Its direct unit test fails. If close were never asserted and the application had no observable consequence, merely running cleanup code would not prove that cleanup worked. Test the responsibility at a meaningful boundary. Do not acquire several unrelated resources before installing any cleanup. If the second acquisition raises, the first may leak. Prefer small composable fixtures or an ExitStack when acquisition is dynamic. Do not let an assertion in the cleanup obscure an earlier failure without a clear diagnostic.
Open in local Python playground ↗

Independent practice

First run the fixture and find the three phase reports for the resource test.

First run the fixture and find the three phase reports for the resource test. Then add an idempotent-close test: closing the resource twice should leave it closed without an exception. This is a documented policy of this toy resource, not a universal requirement for every library handle.

The new test should construct its own resource rather than reuse an object from another test. Type the small try/finally arrangement after reading the entire example. Optional exploration: deliberately raise in the test body and observe that teardown still appears.

Step 1: Add an idempotent double-close test for the owned resource. Step 2: Keep the existing passing cases. Add the smallest new assertion first; predict the failure before running pytest.

Practice starter: solve before reading the solution

python

import pytest


class Resource:
    def __init__(self):
        self.closed = False

    def close(self):
        self.closed = True


@pytest.fixture
def resource():
    value = Resource()
    try:
        yield value
    finally:
        value.close()


def test_resource_is_open_during_call(resource):
    assert resource.closed is False


def test_close_releases_resource():
    value = Resource()
    value.close()
    assert value.closed is True

# Task: Add an idempotent double-close test for the owned resource.
Reasoning
Add an idempotent double-close test for the owned resource.
Open in local Python playground ↗
  • Hint 1: Construct a new Resource inside the test.
  • Hint 2: Close it twice and assert the final state.

Solution, tests and trade-offs

The solution adds a double-close test.

The solution adds a double-close test. Its probe introduces an exception only on a second close. Existing one-close and fixture-lifetime checks still pass, while the new test rejects the changed policy. The exercise therefore adds evidence about a distinct lifecycle boundary.

Production resource tests should avoid touching real databases or services unnecessarily. These examples use owned in-memory state. A browser cannot demonstrate operating-system process cleanup, so that claim belongs in an appropriate local integration test rather than in this simulated resource.

Practice solution and acceptance checks

python

import pytest


class Resource:
    def __init__(self):
        self.closed = False

    def close(self):
        self.closed = True


@pytest.fixture
def resource():
    value = Resource()
    try:
        yield value
    finally:
        value.close()


def test_resource_is_open_during_call(resource):
    assert resource.closed is False


def test_close_releases_resource():
    value = Resource()
    value.close()
    assert value.closed is True

def test_close_is_idempotent():
    value = Resource()
    value.close()
    value.close()
    assert value.closed is True
Reasoning
The solution adds a double-close test. Its probe introduces an exception only on a second close. Existing one-close and fixture-lifetime checks still pass, while the new test rejects the changed policy. The exercise therefore adds evidence about a distinct lifecycle boundary. Production resource tests should avoid touching real databases or services unnecessarily. These examples use owned in-memory state. A browser cannot demonstrate operating-system process cleanup, so that claim belongs in an appropriate local integration test rather than in this simulated resource.
Open in local Python playground ↗

Interview reasoning and next step

Explain the difference between setup failure, test-call failure and teardown failure.

Explain the difference between setup failure, test-call failure and teardown failure. State when code after yield is reached and why placing acquisition inside a suitable context manager can simplify cleanup. Describe a way to test cleanup without relying on the order of unrelated tests.

Next use pytest’s tmp_path for files. The same ownership rule applies: the test should know which files belong to it and should not use a production path merely because it is convenient.

Optional stretch — after the core task works: Introduce one deliberate implementation mistake and explain which assertion catches it. A green run alone is not evidence that every possible bug is covered.

Computer lab: https://applydjinn.com/typorithm/pytest-lab-20261106.zip. Run verify.py in an isolated environment. Worked examples and solutions pass, controlled counterexamples fail, and the new practice assertions reject a mutation missed by the baseline. Run --practice pyq-cleanup after changing the supplied exercise. Python executes on your computer or in the browser, without submitting code to a server. Native question progress is a separate learning record.

Own a resource, expose it for a test, and release it even when that test fails.