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 identical by Python:
>>>True==1==1.0True
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 int
1. 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
).
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!
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.
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 hat for the sake of clarity 😉 ↩