58

I have some tests in Python written in unittest. I want to check that some of my dictionaries contain at least certain attributes equal to certain values. If there are extra values, that would be fine. assertDictContainsSubset would be perfect, except that it's deprecated. Is there a better thing that I should be using or should I just recursively assert the contents to be equal if they are in the target dictionary?

The docs recommend using addTypeEqualityFunc, but I do want to use the normal assertEqual for dicts in some cases.

bman
  • 5,016
  • 4
  • 36
  • 69
Alphadelta14
  • 2,854
  • 3
  • 16
  • 19

4 Answers4

48

On Python 3.9+, use the dictionary union operator.

Change

assertDictContainsSubset(a, b)

to

assertEqual(b, b | a)

On older versions of Python, change it to

assertEqual(b, {**b, **a})

Note the order of the arguments, assertDictContainsSubset put the "larger" dictionary (b) second and the subset (a) first, but it makes more sense to put the larger dictionary (b) first (which is why assertDictContainsSubset was removed in the first place).

This creates a copy of b then iterates over a, setting any keys to their value in a and then compares that result against the original b. If you can add all the keys/values of a to b and still have the same dictionary, it means a doesn't contain any keys that aren't in b and all the keys it contains have the same values as they do in b, i.e. a is a subset of b.

Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
  • If comparing `dict`s, use `assertDictEqual` instead of `assertEqual` for better messages on failure. – Paul Price Jun 15 '21 at 18:52
  • 5
    @PaulPrice the [documentation for `assertDictEqual`](https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertDictEqual) says "This method will be used by default to compare dictionaries in calls to `assertEqual()`" and [also](https://docs.python.org/3/library/unittest.html#unittest.TestCase.addTypeEqualityFunc) "it’s usually not necessary to invoke these methods directly". – Boris Verkhovskiy Jun 15 '21 at 19:07
  • @BorisVerkhovskiy in order of arguments, it's better to use `assertEqual(b, a | b)` or `assertEqual(b, {**a, **b})`, [https://peps.python.org/pep-0584/#specification](based on the doc), If a key appears in both operands, the last-seen value (i.e. that from the right-hand operand) wins. – mosi_kha Nov 27 '22 at 09:46
  • 1
    Which is what we want. If `a` has a different value for some key then it’s not a subset of `b`. To ignore the values do `a.keys().issubset(b.keys())` or `a.keys() <= b.keys()` instead. – Boris Verkhovskiy Nov 27 '22 at 09:49
15

If you were testing if dict A is a subset of dict B, I think I would write a function that tries to extract the content of dict A from dict B making a new dict C and then assertEqual(A,C).

def extractDictAFromB(A,B):
    return dict([(k,B[k]) for k in A.keys() if k in B.keys()])

then you could just do

assertEqual(A,extractDictAFromB(A,B))
Andrew Robinson
  • 346
  • 4
  • 11
  • The good thing about this answer compared to others I've seen is that it allows you to get the nice reporting that assertEqual (or assert_equal in nosetests) does. – arhuaco May 04 '15 at 06:44
  • what is called `extractDictAFromB` here is actually `intersection`. – mehmet Jun 19 '20 at 18:37
13

Extending on @bman's answer, exploiting that the comparison operators for set-like objects are overloaded as subset operators, you can use assertGreaterEqual for (arguably) better error messages.

Compare the two tests:

import unittest

class SubsetTestCase(unittest.TestCase):
    def test_dict_1(self):
        a = {1: 1, 2: 2}
        b = {1: 2}
        self.assertTrue(a.items() >= b.items())

    def test_dict_2(self):
        a = {1: 1, 2: 2}
        b = {1: 2}
        self.assertGreaterEqual(a.items(), b.items())

unittest.main()

The result is:

======================================================================
FAIL: test_dict_1 (__main__.SubsetTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 9, in test_dict_1
    self.assertTrue(a.items() >= b.items())
AssertionError: False is not true

======================================================================
FAIL: test_dict_2 (__main__.SubsetTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 15, in test_dict_2
    self.assertGreaterEqual(a.items(), b.items())
AssertionError: dict_items([(1, 1), (2, 2)]) not greater than or equal to dict_items([(1, 2)])

----------------------------------------------------------------------

With assertGreaterEqual, you can see the contents of the two dictionaries from the error message.

Ignatius
  • 2,745
  • 2
  • 20
  • 32
  • Does not work with python 3. error `'>=' not supported between instances of 'dict' and 'dict'` – Pramod Jangam May 21 '21 at 12:03
  • @PramodJangam This answer does not suggest comparing `dict` to `dict`, but `dict.items()` to `dict.items()`. `dict.items()` returns a set-like object which is comparable to one another. – Ignatius May 24 '21 at 06:29
9

Andrew has offered a solution that uses assertEqual. But, it is useful for future readers, to know two alternative solutions that are more concise. First one uses issubset method of a set:

assert set(A.items()).issubset(set(B.items()))

But there is yet another simpler more Pythonic way to do this:

set(A.items()) <= set(B.items())

The pitfall of the second solution is that you would not know which keys of the superset are missing from the subset.

However, both solutions would fail if your values have unhashable variables (such as dict) inside them.

bman
  • 5,016
  • 4
  • 36
  • 69
  • For the last line you really want `set(A.items()) <= set(B.items())` otherwise it does a list comparison if I am correct. Finally, this will unfortunately fail if any value is not hashable. – Olivier Melançon Apr 14 '18 at 04:49
  • 4
    @OlivierMelançon you don't need the `set()`, because `dict.items()` are already "set-like". See: https://docs.python.org/3/library/stdtypes.html#dict-views – Ignatius Aug 07 '19 at 02:45