4

I'm having a little bit of trouble comparing two similar dictionaries. I would like stricter comparison of the values (and probably keys).

Here's the really basic problem:

>>> {'a': True} == {'a': 1}
True

Similarly (and somewhat confusingly):

>>> {1: 'a'} == {True: 'a'}
True

This makes sense because True == 1. What I'm looking for is something that behaves more like is, but compares two possibly nested dictionaries. Obviously you can't use use is on the two dictionaries, because that will always return False, even if all of the elements are identical.

My current solution is to just use json.dumps to get a string representation of both and compare that.

>>> json.dumps({'a': True}, sort_keys=True) == json.dumps({'a': 1}, sort_keys=True)
False

But this only works if everything is JSON-serializable.

I also tried comparing all of the keys and values manually:

>>> l = {'a': True}
>>> r = {'a': 1}
>>> r.keys() == l.keys() and all(l[key] is r[key] for key in l.keys())
False

But this fails if the dictionaries have some nested structure. I figured I could write a recursive version of this to handle the nested case, but it seemed unnecessarily ugly and un-pythonic.

Is there a "standard" or simple way of doing this?

Thanks!

John P
  • 65
  • 7
  • 4
    This doesn't seem like a good idea - small integers and strings that are valid identifiers compare OK with `is` because they're interned (in CPython at least), but you will quickly hit issues with anything non-trivial. What is the underlying problem that you were trying to solve that led you to `True` vs. `1` as a dictionary key? – jonrsharpe Aug 25 '17 at 19:49
  • some sample dictionaries with the nested cases you want to find would be nice – Alter Aug 25 '17 at 19:54
  • I have a `JSONField` in Django that stores some denormalized data. (It's product data). Having `{"assembly_required": True}` makes it clear that the product requires assembly, but `{"assembly_required": 1}` is slightly less clear. DeepDiff looks like it will work. Just need to do a `DeepDiff(left, right) == {}`. – John P Aug 25 '17 at 20:26
  • 1
    Hmm, not sure why the original comment mentioning [DeepDiff](https://github.com/seperman/deepdiff) was deleted, but OP didn't pull it out of thin air: https://stackoverflow.com/a/26171760/95852 – John Y Aug 25 '17 at 22:56

4 Answers4

2

You were pretty close with JSON: Use Python's pprint module instead. This is documented to sort dictionaries in Python 2.5+ and 3:

Dictionaries are sorted by key before the display is computed.

Let's confirm this. Here's a session in Python 3.6 (which conveniently preserves insertion order even for regular dict objects):

Python 3.6.2 (v3.6.2:5fd33b5, Jul  8 2017, 04:57:36) [MSC v.1900 64 bit (AMD64)]
 on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> a = {2: 'two', 3: 'three', 1: 'one'}
>>> b = {3: 'three', 2: 'two', 1: 'one'}
>>> a
{2: 'two', 3: 'three', 1: 'one'}
>>> b
{3: 'three', 2: 'two', 1: 'one'}
>>> a == b
True
>>> c = {2: 'two', True: 'one', 3: 'three'}
>>> c
{2: 'two', True: 'one', 3: 'three'}
>>> a == b == c
True
>>> from pprint import pformat
>>> pformat(a)
"{1: 'one', 2: 'two', 3: 'three'}"
>>> pformat(b)
"{1: 'one', 2: 'two', 3: 'three'}"
>>> pformat(c)
"{True: 'one', 2: 'two', 3: 'three'}"
>>> pformat(a) == pformat(b)
True
>>> pformat(a) == pformat(c)
False
>>>

And let's quickly confirm that pretty-printing sorts nested dictionaries:

>>> a['b'] = b
>>> a
{2: 'two', 3: 'three', 1: 'one', 'b': {3: 'three', 2: 'two', 1: 'one'}}
>>> pformat(a)
"{1: 'one', 2: 'two', 3: 'three', 'b': {1: 'one', 2: 'two', 3: 'three'}}"
>>>

So, instead of serializing to JSON, serialize using pprint.pformat(). I imagine there may be some corner cases where two objects that you want to consider unequal nevertheless create the same pretty-printed representation. But those cases should be rare, and you wanted something simple and Pythonic, which this is.

John Y
  • 14,123
  • 2
  • 48
  • 72
1

You can test identity of all (key, value) pairs element-wise:

def equal_dict(d1, d2):
    return all((k1 is k2) and (v1 is v2)
               for (k1, v1), (k2, v2) in zip(d1.items(), d2.items()))

>>> equal_dict({True: 'a'}, {True: 'a'})
True

>>> equal_dict({1: 'a'}, {True: 'a'})
False

This should work with float, int, str and bool, but not other sequences or more complex objects. Anyway, that's a start if you need it.

FabienP
  • 3,018
  • 1
  • 20
  • 25
  • This doesn't work, because you are testing with `is`. Your tests happen to give the desired results due to interning and possibly other Python micro-optimizations. But it can incorrectly report two equal dictionaries as unequal. For example, instead of `'a'`, use two separate variables, and give those variables equal-but-not-interned values, such as large integers. – John Y Aug 25 '17 at 22:38
  • And, as you already said, it doesn't work for "sequences or more complex objects". So it doesn't meet OP's requirement that it handle nested structures. – John Y Aug 25 '17 at 22:44
0

You can use isinstance() to delineate between a regular dictionary entry and a nested dictionary entry. This way you can iterate through using is to compare strictly, but also check when you need to dive down a level into the nested dictionary.

https://docs.python.org/3/library/functions.html#isinstance

myDict = {'a': True, 'b': False, 'c': {'a': True}}
for key, value in myDict.items():
    if isinstance(value, dict):
        # do what you need to do....
    else:
        # etc...
sophros
  • 14,672
  • 11
  • 46
  • 75
blake
  • 76
  • 1
  • 4
0

I think you are looking for something like this. However since you didn't provide example data I won't go into guessing what it could be

from boltons.itertools import remap

def compare(A, B): return A == B and type(A) == type(B)

dict_to_compare_against = { some dict }

def visit(path, key, value):
    cur = dict_to_compare_against
    for i in path:
        cur = cur[i]

    if not compare(cur, value):
        raise Exception("Not equal")


remap(other_dict, visit=visit)
Joonatan Samuel
  • 641
  • 4
  • 17