Matt White

Matt White

developer

Matt White

developer

| blog
| categories
| tags
| rss

Inexact Number Comparisons in Python

It becomes tricky to write correct comparison logic when your numbers may not be exactly the same. Working with anything more than basic metrics and contrived data will often result in the need to go beyond a basic comparison:

# python
a == b

# unittest
self.assertEqual(a, b)

# pytest
assert a == b

Photo by chuttersnap on Unsplash

Tolerant Comparisons in Tests

If you’re focused on tests and using pytest, you’re golden - pytest.approx should have you covered:

import pytest
assert a == pytest.approx(b)

Unfortunately, most of my active projects use unittest, so it’s natural to try to turn to assertAlmostEqual to compare:

# unittest: make sure that a and b are equal up to 2 decimal places.
self.assertAlmostEqual(a, b, 2)

This works well enough for confined number ranges, but test cases often involve a large variety of different metrics running through the same assertions - assertAlmostEqual falls pretty flat when you’re using it to compare number ranges from 0.000001 up to 100000000000.

# This evaluates as True despite the values being quite different on a relative basis
self.assertAlmostEqual(0.000001, 0.009, 2)

# This evaluates as False despite the values being virtually identical
self.assertAlmostEqual(100000000000, 100000000000.01, 2)

math.isclose for Relative Tolerances

Obviously you can tailor every assertion to the data, but if you’re looking for something closer to pytest’s approx (and available without importing a testing framework), I’ve taken to using math.isclose.

isclose allows you to compare values using a relative tolerance rather than a fixed number of decimal places:

import math

# will return True if a and b are within 0.1% of each other
math.isclose(a, b, rel_tol=0.001)

You can use the function directly or drop it into a test case helper function for your unittest or Django test classes:

import unittest
import math

class TestWhatever(unittest.TestCase):
    def assert_is_close(self, a, b, rel_tol=0.001):
        self.assertTrue(
            math.isclose(a, b, rel_tol=rel_tol),
            # custom message, as the default failure message won't be informative
            f"'{a}' is not within {rel_tol:.2%} of '{b}'"
        )

Unchecked Speculation on Proposed Python Ideas

The day this post first went out, I saw some discussion on the python-ideas mailing list about introducing a kind of plus-or-minus operator into Python’s grammar, e.g.

a == b +- 0.05

This would evaluate to true if b was within 0.05 above or below a.

While I think the syntax is pretty intuitive, I’m not sure how useful it is. As discussed above, absolute value comparisons aren’t very useful for numbers that aren’t within a confined range. Maybe if there was a clean way to indicate if it’s a relative comparison, e.g.

# for absolute comparisons
a == b +- 0.05

# for relative comparisons
a == b +- 0.01% 

But it seems like a needless complication when math.isclose is simple enough to use. We don’t need another walrus operator debacle.

Learn by doing.