22

I am trying to assert that two dictionaries are almost equal, but I can't seem to do that.

Here is an example:

>>> import nose.tools as nt >>> nt.assert_dict_equal({'a' : 12.4}, {'a' : 5.6 + 6.8}) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/unittest/case.py", line 838, in assertDictEqual self.fail(self._formatMessage(msg, standardMsg)) File "/usr/lib/python2.7/unittest/case.py", line 413, in fail raise self.failureException(msg) AssertionError: {'a': 12.4} != {'a': 12.399999999999999} - {'a': 12.4} + {'a': 12.399999999999999} 

I would like this to pass, like that:

>>> nt.assert_almost_equal(12.4, 5.6 + 6.8) 

I am hoping that I missing something simple like, nt.assert_almost_dict_equal, or maybe there is parameter that I could pass to nt.assert_dict_equal that specifies how close floating points should be, but I can't find anything.

Of course, I could just loop over the dictionaries and use nt.assert_almost_equal to compare the values individually; however, in my case the dictionary is more complicated, so I was hoping to avoid that.

What is the best way to assert that two dictionaries are almost equal?

3
  • 2
    I think you will need to iterate and compare the values yourself. assert_almost_equal is only provided for numeric types whose difference can be directly computed. Commented May 8, 2014 at 18:20
  • 7
    If you find you need to roll your own, check out "assertDeepAlmostEqual" here: github.com/larsbutler/oq-engine/blob/master/tests/utils/… Commented May 8, 2014 at 18:20
  • @dano, that's interesting thanks. Commented May 8, 2014 at 18:22

5 Answers 5

22

The comment by @dano answered my question:

I copied a function from a link provided by dano

import unittest import numpy def assertDeepAlmostEqual(test_case, expected, actual, *args, **kwargs): """ Assert that two complex structures have almost equal contents. Compares lists, dicts and tuples recursively. Checks numeric values using test_case's :py:meth:`unittest.TestCase.assertAlmostEqual` and checks all other values with :py:meth:`unittest.TestCase.assertEqual`. Accepts additional positional and keyword arguments and pass those intact to assertAlmostEqual() (that's how you specify comparison precision). :param test_case: TestCase object on which we can call all of the basic 'assert' methods. :type test_case: :py:class:`unittest.TestCase` object """ is_root = not '__trace' in kwargs trace = kwargs.pop('__trace', 'ROOT') try: if isinstance(expected, (int, float, long, complex)): test_case.assertAlmostEqual(expected, actual, *args, **kwargs) elif isinstance(expected, (list, tuple, numpy.ndarray)): test_case.assertEqual(len(expected), len(actual)) for index in xrange(len(expected)): v1, v2 = expected[index], actual[index] assertDeepAlmostEqual(test_case, v1, v2, __trace=repr(index), *args, **kwargs) elif isinstance(expected, dict): test_case.assertEqual(set(expected), set(actual)) for key in expected: assertDeepAlmostEqual(test_case, expected[key], actual[key], __trace=repr(key), *args, **kwargs) else: test_case.assertEqual(expected, actual) except AssertionError as exc: exc.__dict__.setdefault('traces', []).append(trace) if is_root: trace = ' -> '.join(reversed(exc.traces)) exc = AssertionError("%s\nTRACE: %s" % (exc.message, trace)) raise exc # My part, using the function class TestMyClass(unittest.TestCase): def test_dicts(self): assertDeepAlmostEqual(self, {'a' : 12.4}, {'a' : 5.6 + 6.8}) def test_dicts_2(self): dict_1 = {'a' : {'b' : [12.4, 0.3]}} dict_2 = {'a' : {'b' : [5.6 + 6.8, 0.1 + 0.2]}} assertDeepAlmostEqual(self, dict_1, dict_2) def main(): unittest.main() if __name__ == "__main__": main() 

Result:

Ran 2 tests in 0.000s OK 
Sign up to request clarification or add additional context in comments.

6 Comments

Downvoter, can you explain the reason for the downvote. I believe self-answers are fine. Do you think I am not giving enough credit to @dano?
It's probably bad style, but if you monkey-patch TestCase via 'unittest.TestCase.assertDeepAlmostEqual = assertDeepAlmostEqual` then you can use the test like any other, e.g. self.assertDeepAlmostEqual(dict_1, dict_2)
Should also note that in my case I had to sort all nested collection, so you could use the code here together with this code snippet for ordering nested collection
No need to monkey patch TestCase. You can simply subclass it. Indeed that's what testtools.TestCase does.
Python 2 is no longer supported so I would replace xrange with range. Also long should be numpy.long or you can explicitly from numpy import long, ndarray and save importing all of numpy.
|
5

Pytest "approx" does the job

In [10]: {'a': 2.000001} == pytest.approx({'a': 2}) Out[10]: True

Comments

3

I realize you wouldn't import pandas just to do this but if you happen to be using pandas you can convert the dicts to series and use the assert_series_equal from pandas.testing which, by default, has check_exact=False.

>>> import pandas as pd >>> from pandas.testing import assert_series_equal >>> a = pd.Series({'a' : 12.4}) >>> b = pd.Series({'a': 12.399999999999999}) >>> assert_series_equal(a, b) 

Comments

1

I couldn't get Akavall's function to run so made my own. Is a little too simple but works for my purposes. Code to test that function is working written using pytest

from numbers import Number from math import isclose def dictsAlmostEqual(dict1, dict2, rel_tol=1e-8): """ If dictionary value is a number, then check that the numbers are almost equal, otherwise check if values are exactly equal Note: does not currently try converting strings to digits and comparing them. Does not care about ordering of keys in dictionaries Just returns true or false """ if len(dict1) != len(dict2): return False # Loop through each item in the first dict and compare it to the second dict for key, item in dict1.items(): # If it is a nested dictionary, need to call the function again if isinstance(item, dict): # If the nested dictionaries are not almost equal, return False if not dictsAlmostEqual(dict1[key], dict2[key], rel_tol=rel_tol): return False # If it's not a dictionary, then continue comparing # Put in else statement or else the nested dictionary will get compared twice and # On the second time will check for exactly equal and will fail else: # If the value is a number, check if they are approximately equal if isinstance(item, Number): # if not abs(dict1[key] - dict2[key]) <= rel_tol: # https://stackoverflow.com/questions/5595425/what-is-the-best-way-to-compare-floats-for-almost-equality-in-python if not isclose(dict1[key], dict2[key], rel_tol=rel_tol): return False else: if not (dict1[key] == dict2[key]): return False return True 

Validate function output using pytest

import pytest import dictsAlmostEqual def test_dictsAlmostEqual(): a = {} b = {} assert dictsAlmostEqual(a, b) a = {"1": "a"} b = {} assert not dictsAlmostEqual(a, b) a = {"1": "a"} b = {"1": "a"} assert dictsAlmostEqual(a, b) a = {"1": "a"} b = {"1": "b"} assert not dictsAlmostEqual(a, b) a = {"1": "1.23"} b = {"1": "1.23"} assert dictsAlmostEqual(a, b) a = {"1": "1.234"} b = {"1": "1.23"} assert not dictsAlmostEqual(a, b) a = {"1": 1.000000000000001, "2": "a"} b = {"1": 1.000000000000002, "2": "a"} assert not dictsAlmostEqual(a, b, rel_tol=1e-20) assert dictsAlmostEqual(a, b, rel_tol=1e-8) assert dictsAlmostEqual(a, b) # Nested dicts a = {"1": {"2": 1.000000000000001}} b = {"1": {"2": 1.000000000000002}} assert not dictsAlmostEqual(a, b, rel_tol=1e-20) assert dictsAlmostEqual(a, b, rel_tol=1e-8) assert dictsAlmostEqual(a, b) a = {"1": {"2": 1.000000000000001, "3": "a"}, "2": "1.23"} b = {"1": {"2": 1.000000000000002, "3": "a"}, "2": "1.23"} assert not dictsAlmostEqual(a, b, rel_tol=1e-20) assert dictsAlmostEqual(a, b, rel_tol=1e-8) assert dictsAlmostEqual(a, b) 

Comments

0

I needed a recursive version of round for pretty printing:

def recursive_round(data, ndigits=5): if isinstance(data, dict): return {k: recursive_round(v, ndigits) for k, v in data.items()} if isinstance(data, list): return [recursive_round(v, ndigits) for v in data] if isinstance(data, tuple): return tuple(recursive_round(v, ndigits) for v in data) if isinstance(data, set): return {recursive_round(v, ndigits) for v in data} if isinstance(data, float): if data.is_integer(): return int(data) return round(data, ndigits) return data 

as an example:

from pprint import pprint import math DATA = { 'test': [1.23456, 'whatever', (2.34567, math.pi)], 'another_key': [1.0, 2.0, 0.0, {math.e, math.inf}], 'last_key': [0.123456789, 9.87654321] } pprint(recursive_round(DATA, 3)) # {'another_key': [1, 2, 0, {2.718, inf}], # 'last_key': [0.123, 9.877], # 'test': [1.235, 'whatever', (2.346, 3.142)]} 

It can be used for unittests too, with assertEqual:

import unittest import math def recursive_round(data, ndigits=5): if isinstance(data, dict): return {k: recursive_round(v, ndigits) for k, v in data.items()} if isinstance(data, list): return [recursive_round(v, ndigits) for v in data] if isinstance(data, tuple): return tuple(recursive_round(v, ndigits) for v in data) if isinstance(data, set): return {recursive_round(v, ndigits) for v in data} if isinstance(data, float): if data.is_integer(): return int(data) return round(data, ndigits) return data DATA = { 'test': [1.23456, 'whatever', (2.34567, math.pi)], 'another_key': [1.0, 2.0, 0.0, {math.e, math.inf}], 'last_key': [0.123456789, 9.87654321] } class TestClass(unittest.TestCase): def test_method(self): expected = {'another_key': [1, 2, 0, {2.718, math.inf}], 'last_key': [0.123, 9.877], 'test': [1.235, 'whatever', (2.346, 3.142)]} self.assertEqual(expected, recursive_round(DATA, 3)) if __name__ == '__main__': unittest.main() 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.