Why are tuples constructed from differently initialized sets equal?

Each Answer to this Q is separated by one/two green lines.

I expected the following two tuples

>>> x = tuple(set([1, "a", "b", "c", "z", "f"]))
>>> y = tuple(set(["a", "b", "c", "z", "f", 1]))

to compare unequal, but they don’t:

>>> x == y
>>> True

Why is that?

At first glance, it appears that x should always equal y, because two sets constructed from the same elements are always equal:

>>> x = set([1, "a", "b", "c", "z", "f"])
>>> y = set(["a", "b", "c", "z", "f", 1])
>>> x
{1, 'z', 'a', 'b', 'c', 'f'}
>>> y
{1, 'z', 'a', 'b', 'c', 'f'}
>>> x == y
True

However, it is not always the case that tuples (or other ordered collections) constructed from two equal sets are equal.

In fact, the result of your comparison is sometimes True and sometimes False, at least in Python >= 3.3. Testing the following code:

# compare.py
x = tuple(set([1, "a", "b", "c", "z", "f"]))
y = tuple(set(["a", "b", "c", "z", "f", 1]))
print(x == y)

… a thousand times:

$ for x in {1..1000}
> do
>   python3.3 compare.py
> done | sort | uniq -c
147 False
853 True

This is because, since Python 3.3, the hash values of strings, bytes and datetimes are randomized as a result of a security fix. Depending on what the hashes are, “collisions” may occur, which will mean that the order items are stored in the underlying array (and therefore the iteration order) depends on the insertion order.

Here’s the relevant bit from the docs:

Security improvements:

  • Hash randomization is switched on by default.

https://docs.python.org/3/whatsnew/3.3.html

EDIT: Since it’s mentioned in the comments that the True/False ratio above is superficially surprising …

Sets, like dictionaries, are implemented as hash tables – so if there’s a collision, the order of items in the table (and so the order of iteration) will depend both on which item was added first (different in x and y in this case) and the seed used for hashing (different across Python invocations since 3.3). Since collisions are rare by design, and the examples in this question are smallish sets, the issue doesn’t arise as often as one might initially suppose.

For a thorough explanation of Python’s implementation of dictionaries and sets, see The Mighty Dictionary.

There are two things at play here.

  1. Sets are unordered. set([1, "a", "b", "c", "z", "f"])) == set(["a", "b", "c", "z", "f", 1])

  2. When you convert a set to a tuple via the tuple constructor it essentially iterates over the set and adds each element returned by the iteration .

The constructor syntax for tuples is

tuple(iterable) -> tuple initialized from iterable's items

Calling tuple(set([1, "a", "b", "c", "z", "f"])) is the same as calling tuple([i for i in set([1, "a", "b", "c", "z", "f"])])

The values for

[i for i in set([1, "a", "b", "c", "z", "f"])]

and

[i for i in set(["a", "b", "c", "z", "f", 1])]

are the same as it iterates over the same set.

EDIT thanks to @ZeroPiraeus (check his answer ). This is not guaranteed. The value of the iteration will not always be the same even for the same set.

The tuple constructor doesn’t know the order in which the set is constructed.

Sets are not ordered and are defined only by their membership.

For example, set([1, 2]) == set([2, 1])

Tuples are equal if their members at each position are equal, but since the collections the tuples were created from iterate equally (in increasing order), tuples end up being equal as well.

so you have two lists – which have the same content but in different orders, you convert them into sets – which will be equal, as they have the same content.

When you convert those sets into tuples, they will be converted in the same order, as they are the same set, so the tuples will be the same.

This is true in Python2.7 – but from 3.3 onwards when hashes are randomised you will not be able to guarantee this – as the two sets, although equal in content wont neccessarily iterate in the same order.


The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .