Lesson 6 · Pytest deep dive
pytest Tutorial #6: Assertions Deep Dive
On testing real Python code with pytest. pytest.approx, pytest.raises, and pytest.warns. Let's get into it. Plain == on floats is unreliable due to real…
- CoursePytest deep dive
- Lesson6 of 18
- FormatJupyter notebook · 8 code cells
What you'll learn
- The Helper From Last Time
- The Floating-Point Equality Problem
- pytest.approx - Tolerant Float Comparison
- approx With Custom Tolerance
- pytest.raises - Expecting an Exception
- pytest.raises With match - Checking the Message Too
- pytest.warns - Expecting a Warning
- A Real Pattern - Validating Inputs With Precise Errors
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbpytest Deep-Dive, Video 6: Assertions Deep-Dive#
- Video six of an eighteen-part series on testing real Python code with pytest.
- pytest.approx, pytest.raises, and pytest.warns.
- 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: The Floating-Point Equality Problem#
result = 0.1 + 0.2
print(result)
print(result == 0.3)
Part 3: pytest.approx - Tolerant Float Comparison#
code = '''
import pytest
def test_float_addition():
result = 0.1 + 0.2
assert result == pytest.approx(0.3)
def test_float_addition_fails_without_approx():
result = 0.1 + 0.2
assert result == 0.3
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 4: approx With Custom Tolerance#
code = '''
import pytest
def test_approx_with_relative_tolerance():
measured = 101.5
expected = 100.0
assert measured == pytest.approx(expected, rel=0.02)
def test_approx_with_absolute_tolerance():
measured = 0.0009
expected = 0.0
assert measured == pytest.approx(expected, abs=0.001)
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 5: pytest.raises - Expecting an Exception#
code = '''
import pytest
def divide(a, b):
if b == 0:
raise ValueError('cannot divide by zero')
return a / b
def test_divide_by_zero_raises():
with pytest.raises(ValueError):
divide(10, 0)
def test_divide_normal_case():
assert divide(10, 2) == 5
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 6: pytest.raises With match - Checking the Message Too#
code = '''
import pytest
def parse_positive_int(text):
value = int(text)
if value <= 0:
raise ValueError(f'expected a positive integer, got {value}')
return value
def test_parse_negative_raises_with_message():
with pytest.raises(ValueError, match='expected a positive integer'):
parse_positive_int('-5')
def test_parse_non_numeric_raises_value_error():
with pytest.raises(ValueError):
parse_positive_int('abc')
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 7: pytest.warns - Expecting a Warning#
code = '''
import warnings
import pytest
def old_calculate(a, b):
warnings.warn('old_calculate is deprecated, use calculate instead', DeprecationWarning)
return a + b
def test_old_calculate_warns():
with pytest.warns(DeprecationWarning):
result = old_calculate(2, 3)
assert result == 5
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 8: A Real Pattern - Validating Inputs With Precise Errors#
code = '''
import pytest
def apply_discount(price, percent_off):
if price < 0:
raise ValueError('price cannot be negative')
if not (0 <= percent_off <= 100):
raise ValueError('percent_off must be between 0 and 100')
return price * (1 - percent_off / 100)
def test_apply_discount_normal_case():
assert apply_discount(50.0, 10) == pytest.approx(45.0)
def test_apply_discount_negative_price():
with pytest.raises(ValueError, match='price cannot be negative'):
apply_discount(-10, 10)
def test_apply_discount_bad_percent():
with pytest.raises(ValueError, match='percent_off must be between'):
apply_discount(50, 150)
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Wrap-Up: What You Learned#
- Plain == on floats is unreliable due to real floating-point rounding error.
- pytest.approx wraps an expected value, tolerating small floating-point differences automatically.
- rel and abs keyword arguments on approx control relative and absolute tolerance directly.
- pytest.raises wraps a with block and requires the given exception type to actually be raised inside it.
- The match keyword on raises checks the exception message against a regular expression too.
- pytest.warns mirrors raises for warnings, confirming a given warning category actually fires.
- Combining approx and raises-with-match verifies both correct float math and precise error wording.
- That wraps up assertions deep-dive. Next up: conftest.py and Fixture Sharing.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



