Search code examples
pythonpytest

Check if python dictionaries are equal, allowing small difference for floats


For dictionaries without floating point numbers we are using the simple a == b where a and b are python dictionaries. This works well until we end up with a and b containing floating point numbers somewhere within. They are nested dictionaries so I think that is giving pytest.approx trouble.

What we want is something that will tell us that these two dictionaries are equal (or approximately equal, but something that won't fail only on floating point approximations):

{"foo": {"bar": 0.30000001}} == {"foo": {"bar": 0.30000002}}

pytest.approx() is almost what I want, but it doesn't support nested dictionaries. Is there something out there that can do what I want?


Solution

  • You can define your own approximation helper with support for nested dictionaries. Unfortunately, pytest doesn't support enhancement of approx with custom comparators, so you have to write your own function; however, it doesn't need to be too complicated:

    import pytest
    from collections.abc import Mapping
    from _pytest.python_api import ApproxMapping
    
    
    def my_approx(expected, rel=None, abs=None, nan_ok=False):
        if isinstance(expected, Mapping):
            return ApproxNestedMapping(expected, rel, abs, nan_ok)
        return pytest.approx(expected, rel, abs, nan_ok)
    
    
    class ApproxNestedMapping(ApproxMapping):
        def _yield_comparisons(self, actual):
            for k in self.expected.keys():
                if isinstance(actual[k], type(self.expected)):
                    gen = ApproxNestedMapping(
                        self.expected[k], rel=self.rel, abs=self.abs, nan_ok=self.nan_ok
                    )._yield_comparisons(actual[k])
                    for el in gen:
                        yield el
                else:
                    yield actual[k], self.expected[k]
    
        def _check_type(self):
            for key, value in self.expected.items():
                if not isinstance(value, type(self.expected)):
                    super()._check_type()
    

    Now use my_approx instead of pytest.approx:

    def test_nested():
        assert {'foo': {'bar': 0.30000001}} == my_approx({'foo': {'bar': 0.30000002}})