Lesson 2 · Pytest deep dive
pytest Tutorial #2: Fixtures Basics
On testing real Python code with pytest. Fixtures: real reusable setup and teardown, scope, and composition. Let's get into it. @pytest.fixture marks a…
- CoursePytest deep dive
- Lesson2 of 18
- FormatJupyter notebook · 7 code cells
What you'll learn
- The Helper From Last Time
- Why Fixtures - the Manual-Setup Problem
- A Basic Fixture With @pytest.fixture
- yield Fixtures - Setup and Teardown
- Fixture Scope - function, module, session
- Fixture Composition - a Fixture Using Another Fixture
- A Real Pattern - an In-Memory Fake Database Fixture
- Wrap-Up: What You Learned
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbpytest Deep-Dive, Video 2: Fixtures Basics#
- Video two of an eighteen-part series on testing real Python code with pytest.
- Fixtures: real reusable setup and teardown, scope, and composition.
- Let's get into it.
Part 1: The Helper From Last Time#
import pytest, tempfile, os
def run_pytest(code_str, args=None):
with tempfile.NamedTemporaryFile(mode='w', suffix='_test.py', delete=False) as f:
f.write(code_str)
path = f.name
result = pytest.main([path, *(args or [])])
os.unlink(path)
return result
Part 2: Why Fixtures - the Manual-Setup Problem#
code = '''
def test_manual_one():
data = {'a': 1, 'b': 2}
assert sum(data.values()) == 3
def test_manual_two():
data = {'a': 1, 'b': 2}
assert list(data.keys()) == ['a', 'b']
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 3: A Basic Fixture With @pytest.fixture#
code = '''
import pytest
@pytest.fixture
def sample_data():
return {'a': 1, 'b': 2}
def test_fixture_one(sample_data):
assert sum(sample_data.values()) == 3
def test_fixture_two(sample_data):
assert list(sample_data.keys()) == ['a', 'b']
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 4: yield Fixtures - Setup and Teardown#
code = '''
import pytest
events = []
@pytest.fixture
def tracked_resource():
events.append('setup')
resource = {'open': True}
yield resource
events.append('teardown')
resource['open'] = False
def test_resource_is_open(tracked_resource):
assert tracked_resource['open'] is True
events.append('test ran')
'''
exit_code = run_pytest(code, ['-v', '--no-header', '-s'])
print(exit_code)
Part 5: Fixture Scope - function, module, session#
code = '''
import pytest
call_count = {'function': 0, 'module': 0}
@pytest.fixture
def function_scoped():
call_count['function'] += 1
return call_count['function']
@pytest.fixture(scope='module')
def module_scoped():
call_count['module'] += 1
return call_count['module']
def test_one(function_scoped, module_scoped):
assert function_scoped == 1
assert module_scoped == 1
def test_two(function_scoped, module_scoped):
assert function_scoped == 2
assert module_scoped == 1
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 6: Fixture Composition - a Fixture Using Another Fixture#
code = '''
import pytest
@pytest.fixture
def base_config():
return {'env': 'test', 'debug': True}
@pytest.fixture
def client_config(base_config):
cfg = dict(base_config)
cfg['client_id'] = 'demo-client'
return cfg
def test_base_config(base_config):
assert base_config['env'] == 'test'
def test_client_config(client_config):
assert client_config['env'] == 'test'
assert client_config['client_id'] == 'demo-client'
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 7: A Real Pattern - an In-Memory Fake Database Fixture#
code = '''
import pytest
@pytest.fixture
def fake_db():
db = {}
yield db
db.clear()
def add_user(db, name):
user_id = len(db) + 1
db[user_id] = {'name': name}
return user_id
def test_add_user_creates_entry(fake_db):
uid = add_user(fake_db, 'Alice')
assert uid == 1
assert fake_db[1]['name'] == 'Alice'
def test_fake_db_starts_empty_every_test(fake_db):
assert fake_db == {}
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Wrap-Up: What You Learned#
- @pytest.fixture marks a function as reusable setup that tests request by matching parameter name.
- A plain return fixture just hands back a value; every requesting test gets it as an argument.
- A yield fixture splits into setup (before yield) and teardown (after yield), run around each test.
- scope controls how often a fixture reruns: function (default), module, or session.
- A fixture can request another fixture as its own parameter; pytest resolves the whole chain in order.
- Fixtures avoid duplicating setup code and keep tests isolated with fresh state each time.
- That wraps up fixtures basics. Next up: Parametrize.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



