1

I ran into an interesting situation with the unittest.TestCase.assertItemsEqual function under Python 2; posting my findings here for posterity.

The following unit test breaks under Python 2 when it should succeed:

import unittest

class Foo(object):
    def __init__(self, a=1, b=2):
        self.a = a
        self.b = b
    def __repr__(self):
        return '({},{})'.format(self.a, self.b)
    def __eq__(self, other):
        return self.a == other.a and self.b == other.b
    def __lt__(self, other):
        return (self.a, self.b) < (other.a, other.b)

class Test(unittest.TestCase):
    def test_foo_eq(self):
        self.assertEqual(sorted([Foo()]), sorted([Foo()]))
        self.assertItemsEqual([Foo()], [Foo()])

unittest.main()

Here is the output:

======================================================================
FAIL: test_foo_eq (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tsanders/scripts/one_offs/test_unittest_assert_items_equal2.py", line 17, in test_foo_eq
    self.assertItemsEqual([Foo()], [Foo()])
AssertionError: Element counts were not equal:
First has 1, Second has 0:  (1,2)
First has 0, Second has 1:  (1,2)

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

This is pretty confusing since the docs state:

It [assertItemsEqual] is the equivalent of assertEqual(sorted(expected), sorted(actual)) but it works with sequences of unhashable objects as well.

The same test passes under Python 3 (after swapping self.assertItemsEqual for self.assertCountEqual, since the name has changed).

EDIT: After posting this question, I did find this other question that covers the situation where neither __eq__ nor __hash__ is defined.

0x5453
  • 12,753
  • 1
  • 32
  • 61

1 Answers1

1

To get the test passing under both Python 2 and 3, I had to add the line __hash__ = None to Foo.

The assertItemsEqual/assertCountEqual function takes different code paths depending on whether or not the items in each list are hashable. And according to the docs:

If a class does not define a __cmp__() or __eq__() method it should not define a __hash__() operation either; if it defines __cmp__() or __eq__() but not __hash__(), its instances will not be usable in hashed collections.

With that in mind, Python 2 and 3 have different behavior with regard to __hash__ when __eq__ is defined:

  • In Python 2, defining __eq__ does not affect the default-provided __hash__, but trying to use the item in a hashed container results in implementation-defined behavior (e.g. keys may be duplicated because they have different hashes, even if __eq__ would return True).
  • In Python 3, defining __eq__ sets __hash__ to None.

As of Python 2.6, __hash__ can be explicitly set to None to make a class unhashable. In my case, this was required to get assertItemsEqual to use the correct comparison algorithm that relies on __eq__ instead of __hash__.

0x5453
  • 12,753
  • 1
  • 32
  • 61