45

I know assertDictContainsSubset can do this in python 2.7, but for some reason it's deprecated in python 3.2. So is there any way to assert a dict contains another one without assertDictContainsSubset?

This seems not good:

for item in dic2:
    self.assertIn(item, dic)

any other good way? Thanks

laike9m
  • 18,344
  • 20
  • 107
  • 140
JerryCai
  • 1,663
  • 4
  • 21
  • 36
  • 8
    None of the solutions are as nice as `assertDictContainsSubset`. I'm not pleased that this was remove :( – Gezim Jul 18 '18 at 00:09

8 Answers8

39

Although I'm using pytest, I found the following idea in a comment. It worked really great for me, so I thought it could be useful here.

Python 3:

assert dict1.items() <= dict2.items()

Python 2:

assert dict1.viewitems() <= dict2.viewitems()

It works with non-hashable items, but you can't know exactly which item eventually fails.

kepler
  • 1,712
  • 17
  • 18
31
>>> d1 = dict(a=1, b=2, c=3, d=4)
>>> d2 = dict(a=1, b=2)
>>> set(d2.items()).issubset( set(d1.items()) )
True

And the other way around:

>>> set(d1.items()).issubset( set(d2.items()) )
False

Limitation: the dictionary values have to be hashable.

John1024
  • 109,961
  • 14
  • 137
  • 171
  • This works for me, and it's straightforward. Thanks, and `items()` is equals to `iteritems()` right? – JerryCai Jan 11 '14 at 03:51
  • iteritems has been [removed from python3](http://docs.python.org/3.1/whatsnew/3.0.html#views-and-iterators-instead-of-lists) where items returns an iterator. The code above works with both python2 and python3. – John1024 Jan 11 '14 at 03:56
  • 9
    Note that whether or not this works depends upon the content of the dictionaries: if there's something not hashable, like a list, being used as a value, you won't be able to make a `set`. – DSM Jan 11 '14 at 04:51
  • This is fine for unit testing but unsuitable for production code for several reasons. 1. It creates entire new set data structures for the dict items, wasting time and memory. 2. It only works if the dict *values* as well as keys are hashable. – augurar Mar 01 '19 at 07:57
  • 2
    The question was about `assertDictContainsSubset`, which is a unittest convenience function, so a solution which works for unit testing is appropriate – Matt Zimmerman Aug 01 '19 at 01:41
11

The big problem with the accepted answer is that it does not work if you have non hashable values in your objects values. The second thing is that you get no useful output - the test passes or fails but doesn't tell you which field within the object is different.

As such it is easier to simply create a subset dictionary then test that. This way you can use the TestCase.assertDictEquals() method which will give you very useful formatted output in your test runner showing the diff between the actual and the expected.

I think the most pleasing and pythonic way to do this is with a simple dictionary comprehension as such:

from unittest import TestCase


actual = {}
expected = {}

subset = {k:v for k, v in actual.items() if k in expected}
TestCase().assertDictEqual(subset, expected)

NOTE obviously if you are running your test in a method that belongs to a child class that inherits from TestCase (as you almost certainly should be) then it is just self.assertDictEqual(subset, expected)

Sam Redway
  • 7,605
  • 2
  • 27
  • 41
7

John1024's solution worked for me. However, in case of a failure it only tells you False instead of showing you which keys are not matching. So, I tried to avoid the deprecated assert method by using other assertion methods that will output helpful failure messages:

    expected = {}
    response_keys = set(response.data.keys())
    for key in input_dict.keys():
        self.assertIn(key, response_keys)
        expected[key] = response.data[key]
    self.assertDictEqual(input_dict, expected)
Risadinha
  • 16,058
  • 2
  • 88
  • 91
5

You can use assertGreaterEqual or assertLessEqual.

users = {'id': 28027, 'email': 'chungs.lama@gmail.com', 'created_at': '2005-02-13'}
data = {"email": "chungs.lama@gmail.com"}

self.assertGreaterEqual(user.items(), data.items())
self.assertLessEqual(data.items(), user.items())  # Reversed alternative

Be sure to specify .items() or it won't work.

Asclepius
  • 57,944
  • 17
  • 167
  • 143
Bedram Tamang
  • 3,748
  • 31
  • 27
4

In Python 3 and Python 2.7, you can create a set-like "item view" of a dict without copying any data. This allows you can use comparison operators to test for a subset relationship.

In Python 3, this looks like:

# Test if d1 is a sub-dict of d2
d1.items() <= d2.items()

# Get items in d1 not found in d2
difference = d1.items() - d2.items()

In Python 2.7 you can use the viewitems() method in place of items() to achieve the same result.

In Python 2.6 and below, your best bet is to iterate over the keys in the first dict and check for inclusion in the second.

# Test if d1 is a subset of d2
all(k in d2 and d2[k] == d1[k] for k in d1)
augurar
  • 12,081
  • 6
  • 50
  • 65
  • Thanks for the great answer! For Python 3.x, it seems like this answer should be the accepted answer, i.e. `a.items() <= b.items()`. Though I did also upvote @kepler's answer (it did come a couple years earlier), the pre-2.7 explanation is helpful! – dancow Nov 18 '20 at 19:24
3

This answers a little broader question than you're asking but I use this in my test harnesses to see if the container dictionary contains something that looks like the contained dictionary. This checks keys and values. Additionally you can use the keyword 'ANYTHING' to indicate that you don't care how it matches.

def contains(container, contained):
    '''ensure that `contained` is present somewhere in `container`

    EXAMPLES:

    contains(
        {'a': 3, 'b': 4},
        {'a': 3}
    ) # True

    contains(
        {'a': [3, 4, 5]},
        {'a': 3},
    ) # True

    contains(
        {'a': 4, 'b': {'a':3}},
        {'a': 3}
    ) # True

    contains(
        {'a': 4, 'b': {'a':3, 'c': 5}},
        {'a': 3, 'c': 5}
    ) # True

    # if an `contained` has a list, then every item from that list must be present
    # in the corresponding `container` list
    contains(
        {'a': [{'b':1}, {'b':2}, {'b':3}], 'c':4},
        {'a': [{'b':1},{'b':2}], 'c':4},
    ) # True

    # You can also use the string literal 'ANYTHING' to match anything
        contains(
        {'a': [{'b':3}]},
        {'a': 'ANYTHING'},
    ) # True

    # You can use 'ANYTHING' as a dict key and it indicates to match the corresponding value anywhere
    # below the current point
    contains(
        {'a': [ {'x':1,'b1':{'b2':{'c':'SOMETHING'}}}]},
        {'a': {'ANYTHING': 'SOMETHING', 'x':1}},
    ) # True

    contains(
        {'a': [ {'x':1, 'b':'SOMETHING'}]},
        {'a': {'ANYTHING': 'SOMETHING', 'x':1}},
    ) # True

    contains(
        {'a': [ {'x':1,'b1':{'b2':{'c':'SOMETHING'}}}]},
        {'a': {'ANYTHING': 'SOMETHING', 'x':1}},
    ) # True
    '''
    ANYTHING = 'ANYTHING'
    if contained == ANYTHING:
        return True

    if container == contained:
        return True

    if isinstance(container, list):
        if not isinstance(contained, list):
            contained = [contained]
        true_count = 0
        for contained_item in contained:
            for item in container:
                if contains(item, contained_item):
                    true_count += 1
                    break
        if true_count == len(contained):
            return True

    if isinstance(contained, dict) and isinstance(container, dict):
        contained_keys = set(contained.keys())
        if ANYTHING in contained_keys:
            contained_keys.remove(ANYTHING)
            if not contains(container, contained[ANYTHING]):
                return False

        container_keys = set(container.keys())
        if len(contained_keys - container_keys) == 0:
            # then all the contained keys are in this container ~ recursive check
            if all(
                contains(container[key], contained[key])
                for key in contained_keys
            ):
                return True

    # well, we're here, so I guess we didn't find a match yet
    if isinstance(container, dict):
        for value in container.values():
            if contains(value, contained):
                return True

    return False
JnBrymn
  • 24,245
  • 28
  • 105
  • 147
1

Here is a comparison that works even if you have lists in the dictionaries:

superset = {'a': 1, 'b': 2}
subset = {'a': 1}

common = { key: superset[key] for key in set(superset.keys()).intersection(set(subset.keys())) }

self.assertEquals(common, subset)
user1338062
  • 11,939
  • 3
  • 73
  • 67