Dan Bader

A Python Riddle: The Craziest Dict Expression in the West

Let’s pry apart this slightly unintuitive Python dictionary expression to find out what’s going on in the uncharted depths of the Python interpreter.

A while ago I shared this Python one-liner as a “Python riddle” on Twitter and it got some interesting reactions:

>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}

Take a quick moment to think about what this dict expression will evaluate to.

I’ll wait here 😃

Ok, ready?

It’ll evaluate to this dictionary:

{True: 'maybe'}

I’ll admit I was pretty surprised about this result the first time I saw it. But it all makes sense when you investigate what happens step by step.

Where baby dictionaries come from

Let’s think about why we get this (I want to say “slightly unintuitive”) result:

When Python processes our dictionary expression it first constructs a new empty dictionary object; and then assigns the keys and values to it in the order given in the dict expression.

When we break it down our dict expression is equivalent to this sequence of statements:

>>> xs = dict()
>>> xs[True] = 'yes'
>>> xs[1] = 'no'
>>> xs[1.0] = 'maybe'

Now this gets a lot more interesting when we realize that all keys we’re using in this example are considered to be equal by Python:

>>> True == 1 == 1.0

Okay, but–wait a minute here. We can intuitively accept that 1.0 == 1, but why would True be considered equal to 1 as well?

The answer to that is Python treats bool as a subclass of int1. This is the case in Python 2 and Python 3:

The Boolean type is a subtype of the integer type, and Boolean values behave like the values 0 and 1, respectively, in almost all contexts, the exception being that when converted to a string, the strings “False” or “True” are returned, respectively.

So, as far as Python is concerned, True, 1, and 1.0 all represent the same dictionary key. As the interpreter evaluates the dictionary expression it repeatedly overwrites the value for the key True.

But why do we still get True as the key in the final dictionary? Shouldn’t the key also change to 1.0 at the end through the repeated assignments?

To explain this outcome we need to know that Python doesn’t replace the object instance for a key when a new value is assigned to it:

>>> ys = {1.0: 'no'}
>>> ys[True] = 'yes'
>>> ys
{1.0: 'yes'}

This is presumably done as a performance optimization–if the keys are considered identical then why update the original. From this example we saw that the initial True object is never replaced as the key. Therefore the dictionary’s string representation still prints the key as True (instead of 1 or 1.0).

Wait, what about the hash code?

With what we know now it looks like the values in the resulting dict are getting overwritten because they compare as equal. However, it turns out that this effect isn’t caused by the __eq__ equality check alone, either.

Let’s define the following class as our little detective tool:

class AlwaysEquals:
     def __eq__(self, other):
         """Make this object equal to any other object."""
         return True

     def __hash__(self):
         """Give each object a unique hash code."""
         return id(self)

All instances of this class will pretend they’re equal to any other object:

>>> AlwaysEquals() == AlwaysEquals()

>>> AlwaysEquals() == 42

>>> AlwaysEquals() == 'waaat?'

However they will also each return a unique hash value:

>>> [hash(obj) for obj in [AlwaysEquals(), AlwaysEquals(), AlwaysEquals()]]
[4574298968, 4574287912, 4574287072]

That’ll allow us to test if dictionary keys are overwritten based on their equality comparison result alone. And you’ll see that the keys are not getting overwritten even though they always compare as equal:

>>> {AlwaysEquals(): 'yes', AlwaysEquals(): 'no'}
{ <AlwaysEquals object at 0x110a3c588>: 'yes',
  <AlwaysEquals object at 0x110a3cf98>: 'no' }

We can also flip this idea around and check to see if returning the same hash value is enough to cause keys to get overwritten:

>>> class SameHash:
>>>     def __hash__(self):
>>>         """Give each object the same hash code."""
>>>         return 1

>>> a = SameHash()
>>> b = SameHash()

>>> a == b

>>> hash(a), hash(b)
(1, 1)

>>> print({a: 'a', b: 'b'})
{ <SameHash instance at 0x7f7159020cb0>: 'a',
  <SameHash instance at 0x7f7159020cf8>: 'b' }

As this example shows, the “keys get overwritten” effect isn’t caused by the hash value alone either.

A better explanation of what’s going on here is that dictionaries check for equality and compare the hash value to determine if two keys are the same:

An object is hashable if it has a hash value which never changes during its lifetime (it needs a __hash__() method), and can be compared to other objects (it needs an __eq__() method). Hashable objects which compare equal must have the same hash value.

Hashability makes an object usable as a dictionary key and a set member, because these data structures use the hash value internally.

(Source: https://docs.python.org/3/glossary.html#term-hashable)

Umm okay, what’s the executive summary here?

Let’s try and summarize our findings:

{True: 'yes', 1: 'no', 1.0: 'maybe'} evaluates to {True: 'maybe'} because the keys True, 1, and 1.0 all compare as equal and they all have the same hash value.

>>> True == 1 == 1.0

>>> (hash(True), hash(1), hash(1.0))
(1, 1, 1)

That’s how we end up with this slightly surprising result as the dictionary’s final state:

{True: 'yes', 1: 'no', 1.0: 'maybe'} == {True: 'maybe'}

It’s a Python Trick!

» Subscribe to the dbader.org YouTube Channel for more Python tutorials.

I understand that all of this can be a bit mind-boggling at first. Try playing through the examples I gave one by one in a Python REPL. I’m sure it’ll help expand your knowledge of Python!

I love these short one-line examples that teach you a ton about a language once you wrap your head around them. They’re almost like a Zen kōan 😃

There’s one more thing I want to tell you about:

I’ve started a series of these Python “tricks” delivered over email. You can sign up at dbader.org/python-tricks and I’ll send you a new Python trick as a code screenshot every couple of days.

This is still an experiment and a work in progress but I’ve heard some really positive feedback from the developers who’ve tried it out so far.

Thanks to JayR, Murat, and kurashu89 for their feedback on this article.

  1. Yes, this also means you can do things like this and use bools as indexes: ('no', 'yes')[True] == 'yes' But you probably shouldn’t do it for the sake of clarity 😉 

📘 Python Tricks: The Book — A Buffet of Awesome Python Features: My new book that teaches you the coolest aspects of Python with short and easy to digest examples. » Click here to get a free sample chapter

Improve Your Python with a fresh 🐍 Python Trick 💌 every couple of days

🔒 No spam ever. Unsubscribe any time.

This article was filed under: programming, and python.

Related Articles:
Latest Articles:

💓🐍 Love Python? Show It With Some Unique Python Swag Every Pythonista needs a great coffee (or tea!) mug. That’s why my wife Anja and I started an online store with unique mugs for Python devs: » Check it out at nerdlettering.com

← Browse All Articles