pytest
assert almost equal
Python testing
software testing
test automation

pytest assert almost equal

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When testing floating-point calculations in Python, exact equality comparisons often fail due to precision errors inherent in IEEE 754 representation. For example, 0.1 + 0.2 equals 0.30000000000000004, not 0.3. Pytest provides pytest.approx() to handle approximate comparisons cleanly and readably. It works with scalars, lists, dicts, and NumPy arrays, and supports both relative and absolute tolerance.

The Floating-Point Problem

python
1# Exact equality fails with floating-point arithmetic
2assert 0.1 + 0.2 == 0.3
3# AssertionError: assert 0.30000000000000004 == 0.3
4
5# This is not a bug — it's how binary floating-point works
6print(f"{0.1 + 0.2:.20f}")  # 0.30000000000000004441
7print(f"{0.3:.20f}")         # 0.29999999999999998890

Floating-point numbers are stored in binary, and many decimal fractions (like 0.1) have infinite binary representations that get truncated. This makes exact comparison unreliable for computed values.

Using pytest.approx()

python
1import pytest
2
3def test_basic_approx():
4    assert 0.1 + 0.2 == pytest.approx(0.3)  # passes
5
6def test_calculation():
7    result = 1 / 3
8    assert result == pytest.approx(0.3333, rel=1e-3)  # passes with relative tolerance
9
10def test_negative_values():
11    assert -0.1 - 0.2 == pytest.approx(-0.3)  # works with negatives

pytest.approx(expected) wraps the expected value and compares using a default relative tolerance of 1e-6. The assertion reads naturally — the == operator is overloaded to perform approximate comparison.

Relative vs Absolute Tolerance

python
1import pytest
2
3def test_relative_tolerance():
4    # Relative tolerance: |actual - expected| <= rel * |expected|
5    assert 100.001 == pytest.approx(100, rel=1e-4)  # passes: 0.001 <= 0.01
6
7def test_absolute_tolerance():
8    # Absolute tolerance: |actual - expected| <= abs
9    assert 0.001 == pytest.approx(0, abs=1e-2)  # passes: 0.001 <= 0.01
10
11def test_both_tolerances():
12    # When both specified, the comparison passes if EITHER is satisfied
13    assert 0.1 + 0.2 == pytest.approx(0.3, rel=1e-6, abs=1e-12)
14
15def test_near_zero():
16    # Near zero, relative tolerance breaks down — use absolute
17    assert 1e-10 == pytest.approx(0, abs=1e-9)  # passes
18    # assert 1e-10 == pytest.approx(0, rel=1e-6)  # fails: rel * 0 = 0

Relative tolerance scales with the expected value, making it suitable for large numbers. Absolute tolerance is a fixed threshold, necessary when comparing values near zero where relative tolerance produces zero.

Comparing Collections

python
1import pytest
2
3def test_list_approx():
4    result = [0.1 + 0.2, 1/3, 2/3]
5    expected = [0.3, 0.3333333, 0.6666667]
6    assert result == pytest.approx(expected, rel=1e-5)
7
8def test_dict_approx():
9    result = {"x": 0.1 + 0.2, "y": 1/7}
10    expected = {"x": 0.3, "y": 0.142857}
11    assert result == pytest.approx(expected, rel=1e-4)
12
13def test_nested_not_supported():
14    # pytest.approx does NOT support nested structures
15    # result = [[0.1 + 0.2]]
16    # assert result == pytest.approx([[0.3]])  # TypeError
17    pass

pytest.approx() works element-wise on flat lists, tuples, and dicts. It does not support nested collections — for those, compare each level separately.

Using with NumPy

python
1import pytest
2import numpy as np
3
4def test_numpy_array():
5    result = np.array([0.1, 0.2, 0.3]) + np.array([0.2, 0.1, 0.0])
6    expected = np.array([0.3, 0.3, 0.3])
7    assert result == pytest.approx(expected)
8
9def test_numpy_allclose():
10    # NumPy's own function — alternative to pytest.approx
11    result = np.array([1.0001, 2.0002, 3.0003])
12    expected = np.array([1.0, 2.0, 3.0])
13    np.testing.assert_allclose(result, expected, rtol=1e-3)
14
15def test_numpy_scalar():
16    result = np.float64(0.1) + np.float64(0.2)
17    assert result == pytest.approx(0.3)

Both pytest.approx() and np.testing.assert_allclose() work for NumPy arrays. assert_allclose provides more detailed error messages showing which elements differ.

Alternatives to pytest.approx

python
1import math
2import pytest
3
4# math.isclose — standard library
5def test_math_isclose():
6    assert math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)
7
8# unittest.TestCase.assertAlmostEqual — places-based
9import unittest
10
11class TestCalc(unittest.TestCase):
12    def test_almost_equal(self):
13        self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)  # 7 decimal places
14
15# Manual absolute comparison
16def test_manual():
17    result = 0.1 + 0.2
18    assert abs(result - 0.3) < 1e-9
MethodWorks WithTolerance TypeError Messages
pytest.approx()scalars, lists, dicts, numpyrel + absClear, detailed
math.isclose()scalars onlyrel + absBoolean only
assertAlmostEqual()scalars onlydecimal placesunittest style
np.testing.assert_allclose()numpy arraysrel + absShows mismatched elements

Common Pitfalls

  • Using == without pytest.approx() for floats: Bare assert result == 0.3 fails for computed floating-point values. Always wrap the expected value in pytest.approx() when comparing floats in tests.
  • Relying on relative tolerance near zero: When the expected value is zero, rel * |expected| equals zero, so relative tolerance alone always fails. Use abs tolerance for values near zero: pytest.approx(0, abs=1e-9).
  • Comparing nested structures with pytest.approx(): pytest.approx() does not recurse into nested lists or dicts. [[0.3]] inside pytest.approx() raises a TypeError. Flatten the comparison or compare each level separately.
  • Confusing rel and abs interaction: When both rel and abs are specified, the comparison passes if either tolerance is satisfied (logical OR). This means the effective tolerance is the larger of the two, not the smaller.
  • Forgetting that assertAlmostEqual uses decimal places, not tolerance: self.assertAlmostEqual(a, b, places=7) checks that abs(a - b) < 5e-8 (half a unit in the 7th decimal place). This is different from relative tolerance and can be surprising for very large or very small numbers.

Summary

  • Use pytest.approx(expected) for floating-point comparisons in pytest
  • Default relative tolerance is 1e-6; use abs for values near zero
  • Works with scalars, flat lists, flat dicts, and NumPy arrays
  • math.isclose() is the standard library alternative for scalar comparisons
  • np.testing.assert_allclose() provides detailed NumPy array comparison
  • Never use bare == for computed floating-point values in tests

Course illustration
Course illustration

All Rights Reserved.