Testing
Test with No Assertion
Run pytest test_vacuous.py -v. Does the test pass? Add a print statement inside
test_total to confirm that result is being computed. Does a passing test mean
the function is correct?
def total(values):
return sum(values)
def test_total():
result = total([1, 2, 3])
Show explanation
The bug is that the test contains no assertion, so pytest has nothing to check and always reports it as passing. A vacuous test gives false confidence: the function could return any value and the test would still pass. Shows that every test must contain at least one assertion that can actually fail.
Tuple Is Always Truthy
Run pytest test_tupleassert.py -v. Does the test pass? What value does double(4)
actually return? Check the pytest warning in the output.
def double(x):
return x * 3 # deliberate wrong implementation
def test_double():
result = double(4)
expected = 8
assert (result, expected)
Show explanation
The bug is assert (result, expected), which creates a two-element tuple. A
non-empty tuple is always truthy, so the assertion never fails regardless of whether
result == expected. The correct form is assert result == expected without the
enclosing parentheses. Shows how easy it is to write an assertion that looks
plausible but is logically vacuous, and why pytest's warning about this pattern
should not be ignored.
Floating-Point Equality
Run pytest test_floatequal.py -v. Read the assertion error carefully. What value
did running_total actually return?
import pytest
def running_total(values):
total = 0.0
for v in values:
total += v
return total
def test_running_total():
result = running_total([0.1, 0.2, 0.3])
assert result == 0.6
Show explanation
The bug is comparing floating-point results with ==. Repeated addition accumulates
rounding error in IEEE 754 arithmetic, so 0.1 + 0.2 + 0.3 produces
0.6000000000000001, not 0.6. Shows how to use pytest.approx to compare
floats within a tolerance, and why exact equality between computed floats is
unreliable.
pytest.raises Catches the Wrong Exception
Run pytest test_broadexc.py -v. Does the test pass? What exception does
parse_count(None) actually raise? Is that the exception the test intended to check?
import pytest
def parse_count(value):
return int(value)
def test_parse_count_rejects_none():
with pytest.raises(Exception):
parse_count(None)
Show explanation
The bug is using pytest.raises(Exception), which accepts any exception, including
TypeError. The test passes even though parse_count raises a TypeError rather
than the expected ValueError, masking a bug in the function. Shows how to use a
specific exception type in pytest.raises and why catching the base Exception
class in tests hides incorrect behaviour.
Fixture with No Cleanup
Run pytest test_noyield.py -v. The test fails. After it fails, check whether the
temp file still exists. Then change return path to yield path and add
os.remove(path) after the yield. Run again and check the filesystem.
import os
import tempfile
import pytest
@pytest.fixture
def temp_file():
path = tempfile.mktemp()
return path
def test_write_and_read(temp_file):
with open(temp_file, "w") as f:
f.write("hello")
assert os.path.exists(temp_file)
raise RuntimeError("test failed partway through")
Show explanation
The bug is using return in a fixture that needs to perform cleanup. With return,
there is no way to run code after the test finishes, so the temp file is left on disk
whenever the test fails. With yield, pytest runs the code after the yield as
teardown even if the test raises an exception. Shows the difference between
return and yield in pytest fixtures and why yield is necessary for reliable
cleanup.
Test Depends on Execution Order
Run pytest test_orderdep.py -v. Both tests pass. Now run
pytest test_orderdep.py::test_lookup -v to run only the second test. What happens?
import pytest
_registry = {}
def register(name, value):
_registry[name] = value
def lookup(name):
return _registry[name]
def test_register():
register("threshold", 10)
assert lookup("threshold") == 10
def test_lookup():
assert lookup("threshold") == 10
Show explanation
The bug is that test_lookup relies on test_register having populated _registry
first. When test_lookup runs alone or in a different order, _registry is empty
and the test raises a KeyError. Shows why each test must set up its own state
independently rather than relying on side effects from other tests, and how to use
fixtures to provide shared setup.
Mutable Session-Scoped Fixture
Run pytest test_scopemut.py -v. Which test fails? Reverse the order of the two
tests and run again. Does the other test now fail?
import pytest
@pytest.fixture(scope="session")
def user_list():
return []
def test_add_user(user_list):
user_list.append("Alice")
assert len(user_list) == 1
def test_list_starts_empty(user_list):
assert len(user_list) == 0
Show explanation
The bug is scope="session" on a fixture that returns a mutable list. A session-scoped
fixture is created once and reused across every test in the run, so mutations made by
one test are visible to all later tests. Shows the difference between fixture
scopes, why mutable objects should use function scope (the default) rather than
session scope, and how fixture scope bugs often appear only when tests are run in a
particular order.
Assertion After the Raising Call Is Dead Code
Run pytest test_deadassert.py -v. The test passes. Add a print("reached") on
the line after result = parse_positive(0). Run again. Is the print executed?
import pytest
def parse_positive(value):
if value <= 0:
raise ValueError(f"{value} is not positive")
return value
def test_parse_positive_rejects_zero():
with pytest.raises(ValueError):
result = parse_positive(0)
assert result is None
Show explanation
The bug is placing an assertion inside a with pytest.raises() block after the line
that raises. Once parse_positive(0) raises ValueError, execution jumps to the
end of the with block and the assertion is never reached. The test passes because
the exception was raised as expected, but the assertion that was supposed to check a
return value is silently skipped. Shows that assertions on return values must be
placed after the with pytest.raises() block, not inside it.
Missing Return in Tested Function
Run pytest test_missingreturn.py -v. Read the assertion error. What value did
strip_prefix return? Is that what you expected from the code?
def strip_prefix(text, prefix):
if text.startswith(prefix):
text[len(prefix):]
def test_strip_prefix():
result = strip_prefix("ERROR: disk full", "ERROR: ")
assert result == "disk full"
Show explanation
The bug is a missing return in strip_prefix. The slicing expression computes
the correct substring but discards it, so the function returns None. The test then
compares None == "disk full" and fails. Shows how to recognise a missing
return from a None assertion error and how to check function return values as a
first step when a test fails unexpectedly.
sort Returns None
Run pytest test_paramtype.py -v. All three cases fail. Read the assertion error
for the first case. What value did result have?
import pytest
@pytest.mark.parametrize("values,expected", [
([3, 1, 2], [1, 2, 3]),
([9, 5, 7], [5, 7, 9]),
([4, 4, 1], [1, 4, 4]),
])
def test_sort_returns_sorted_list(values, expected):
result = values.sort()
assert result == expected
Show explanation
The bug is calling values.sort() and assigning its return value. list.sort()
sorts the list in place and returns None; the sorted result is in values, not
in result. The assertion then compares None against the expected list and fails.
Shows the difference between list.sort() (in-place, returns None) and
sorted() (returns a new sorted list), and how parametrize makes the pattern of
failure visible across multiple inputs at once.
Test Writes to the Working Directory
Run pytest test_tmpfile.py -v. The test passes. After it finishes, list the files
in your working directory. What file was left behind?
def summarize(values):
return {"count": len(values), "total": sum(values)}
def test_summarize(tmp_path):
data = [10, 20, 30]
result = summarize(data)
import json
with open("output.json", "w") as f:
json.dump(result, f)
with open("output.json") as f:
saved = json.load(f)
assert saved["count"] == 3
assert saved["total"] == 60
Show explanation
The bug is opening "output.json" as a plain filename, which creates the file in
the current working directory. The file is not deleted when the test finishes, so
subsequent runs may read stale data, and different tests that use the same filename
can interfere with each other. Shows how to use the tmp_path fixture, which
provides a per-test temporary directory that pytest removes automatically after
each run.