When you’re writing tests, especially for anything involving external resources like databases, files, or network connections, you absolutely need to clean up after yourself. Otherwise, your tests will start interfering with each other, leaving behind a mess that makes future runs unreliable. pytest has a robust system for this, and understanding its teardown mechanisms is key to writing stable, repeatable tests.

Let’s see pytest in action. Imagine we’re testing a simple file writing utility. We want to ensure that after each test that creates a file, that file is deleted.

# test_file_cleanup.py
import pytest
import os

TEST_FILE = "my_test_file.txt"

def setup_module(module):
    # This runs once before any tests in this module
    if os.path.exists(TEST_FILE):
        os.remove(TEST_FILE)

def teardown_module(module):
    # This runs once after all tests in this module
    if os.path.exists(TEST_FILE):
        os.remove(TEST_FILE)

def test_create_file():
    with open(TEST_FILE, "w") as f:
        f.write("This is a test.")
    assert os.path.exists(TEST_FILE)

def test_file_creation_and_deletion_via_teardown():
    with open(TEST_FILE, "w") as f:
        f.write("Another test.")
    assert os.path.exists(TEST_FILE)

# To demonstrate individual teardown, we'll use a fixture.
# This fixture will be used by the next test.
@pytest.fixture
def file_creator_fixture():
    print(f"\nFixture setup: Creating {TEST_FILE}")
    with open(TEST_FILE, "w") as f:
        f.write("Content from fixture.")
    yield TEST_FILE  # The value yielded is what the test function receives
    print(f"\nFixture teardown: Removing {TEST_FILE}")
    if os.path.exists(TEST_FILE):
        os.remove(TEST_FILE)

def test_using_fixture_for_teardown(file_creator_fixture):
    print(f"Test running: {file_creator_fixture} exists: {os.path.exists(file_creator_fixture)}")
    assert os.path.exists(file_creator_fixture)
    # The file_creator_fixture's teardown will run automatically after this test finishes.

Running pytest -s on this file will show you the output:

============================= test session starts ==============================
platform linux -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x
rootdir: /path/to/your/project
collected 3 items

test_file_cleanup.py
Fixture setup: Creating my_test_file.txt

Test running: my_test_file.txt exists: True
.Fixture teardown: Removing my_test_file.txt

.
.
============================== 3 passed in X.XXs ===============================

Notice how test_create_file and test_file_creation_and_deletion_via_teardown don’t have explicit cleanup. If run alone, they’d leave my_test_file.txt behind. setup_module and teardown_module are global for the file, but test_using_fixture_for_teardown shows the more granular, per-test cleanup provided by fixtures.

The core problem pytest teardown solves is managing the lifecycle of resources your tests depend on, ensuring a clean slate for each test run and preventing state leakage. This is crucial for test isolation.

pytest offers several ways to handle teardown, each with a different scope:

  1. setup_module / teardown_module: These functions run once per module. setup_module executes before any tests in the file, and teardown_module executes after all tests in the file. This is useful for setting up or tearing down resources that are shared across all tests within a single file, like a temporary directory for the whole file.

  2. setup_function / teardown_function: These run before and after each test function in a module. This is a common way to ensure a clean state for every single test. If you need to create a temporary file or database entry for each test, this is a good candidate.

  3. setup_method / teardown_method: Similar to setup_function/teardown_function, but for test methods within classes.

  4. pytest Fixtures: This is the most powerful and idiomatic way in pytest to handle setup and teardown. Fixtures are functions decorated with @pytest.fixture that can provide resources to test functions. The teardown logic is handled within the fixture itself, often using yield.

The yield keyword in a fixture is the magic for teardown. Code before yield runs as setup. Code after yield runs as teardown, guaranteed to execute even if the test fails or raises an exception. The value before yield is what gets passed into the test function.

@pytest.fixture
def db_connection():
    # Setup: Connect to a test database
    conn = connect_to_test_db("test_db_user", "test_password")
    print("\nDatabase connection established.")
    yield conn # The connection object is passed to the test
    # Teardown: Close the connection and clean up
    conn.close()
    drop_test_db_tables()
    print("Database connection closed and tables dropped.")

def test_user_creation(db_connection):
    user_id = db_connection.create_user("alice")
    assert db_connection.get_user(user_id) is not None
    # db_connection fixture's teardown will run automatically here

In this db_connection fixture, connect_to_test_db is the setup. The conn object is yielded to test_user_creation. When test_user_creation finishes, conn.close() and drop_test_db_tables() are executed.

The most common mistake people make is forgetting to clean up resources when setup_function or teardown_function are used, or not using fixtures for stateful tests at all. They might rely solely on setup_module and then wonder why a test fails because a resource created by a previous test in the same module is still there. Fixtures provide a more granular and explicit control, making it much harder to miss cleanup steps.

The next thing you’ll likely run into is managing more complex fixture scopes, like session scope, for resources that should only be set up once for the entire test run, not just per module or per function.

Want structured learning?

Take the full Pytest course →