This is primarily a note to remind myself of this bug the next time I encounter it. This issue is seen on Python version 3.10.6.
I’m a big fan of using the Python and Ipython debuggers, pdb and ipdb respectively, when trying to
debug code. Recently, I was debugging a function. I dropped in a import ipdb;ipdb.set_trace()
right
before a line of code that did dictionary comprehension. For some reason, I ran the dictionary comprehension
directly in the python debugger. Surprisingly, a NameError
came back for one of the variables used
in the dictionary comprehension. This was really odd considering in the same debugger session I could
print the variable that came back with the NameError
. The variable also appeared in locals()
.
You can reproduce this issue with the following code. This code defines a function. Within the function,
we define a dictionary (d1
) and a list of the keys (fltrlist
). The dictionary comprehension allows
us to filter out keys which do not appear in the list fltrlist
.
For right now ignore the list (klist
) we created outside of the function scope.
def somefunc():
fltrlist = ["A", "B"]
d1 = {
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]
}
import pdb;pdb.set_trace()
d2 = {
k: v for k, v in d1.items() if k in fltrlist
}
klist = ["A", "B"]
somefunc()
If you run this piece of code, enter into the debugger, and try to execute the dictionary comprehension
you will get a NameError
for fltrlist
.
➜ python3 scope_test.py
> scope_test.py(10)somefunc()
-> d2 = {
(Pdb) {k: v for k, v in d1.items() if k in fltrlist}
*** NameError: name 'fltrlist' is not defined
(Pdb) print(fltrlist)
['A', 'B']
Let us return to the variable klist
. This variable is defined in the global scope. Interestingly, if we
reference this variable when executing the dictionary comprehension it works.
➜ python3 scope_test.py
> scope_test.py(11)somefunc()
-> d2 = {
(Pdb) {k: v for k, v in d1.items() if k in klist}
{'A': [1, 2, 3], 'B': [4, 5, 6]
So a variable that was in the local scope but not in the global scope returned a NameError
while a
variable in the global scope worked just fine.
Let’s dig a little deeper into pdb. In the back-end, pdb uses Python’s exec()
and compile()
to
execute commands typed into the prompt. We can try these directly with this example.
def somefunc():
fltrlist = ["A", "B"]
d1 = {
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]
}
exec(compile('{k: v for k, v in d1.items() if k in fltrlist}',
'string', 'exec'), globals(), locals())
klist = ["A", "B"]
somefunc()
When we run this we see the same issue.
➜ python3 scope_test.py
Traceback (most recent call last):
File "/home/ebaumer/code/temp/scope_test.py", line 17, in <module>
somefunc()
File "/home/ebaumer/code/temp/scope_test.py", line 10, in somefunc
exec(compile('{k: v for k, v in d1.items() if k in fltrlist}',
File "string", line 1, in <module>
File "string", line 1, in <dictcomp>
NameError: name 'fltrlist' is not defined
If we try with the globally scoped variable klist
, it works!
def somefunc():
fltrlist = ["A", "B"]
d1 = {
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]
}
exec(compile('{k: v for k, v in d1.items() if k in klist}',
'string', 'exec'), globals(), locals())
klist = ["A", "B"]
somefunc()
We can actually go further down this rabbit hole and use Python’s disassembler for bytecode to see what is going on with the dictionary comprehension.
import dis
def somefunc():
fltrlist = ["A", "B"]
d1 = {
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9]
}
dis.dis(compile('{k: v for k, v in d1.items() if k in fltrlist}',
'string', 'exec'))
klist = ["A", "B"]
somefunc()
The output of this reveals something very interesting.
➜ python3 scope_test.py
1 0 LOAD_CONST 0 (<code object <dictcomp> at 0x7f8aa02fa130, file "string", line 1>)
2 LOAD_CONST 1 ('<dictcomp>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (d1)
8 LOAD_METHOD 1 (items)
10 CALL_METHOD 0
12 GET_ITER
14 CALL_FUNCTION 1
16 POP_TOP
18 LOAD_CONST 2 (None)
20 RETURN_VALUE
Disassembly of <code object <dictcomp> at 0x7f8aa02fa130, file "string", line 1>:
1 0 BUILD_MAP 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 11 (to 28)
6 UNPACK_SEQUENCE 2
8 STORE_FAST 1 (k)
10 STORE_FAST 2 (v)
12 LOAD_FAST 1 (k)
14 LOAD_GLOBAL 0 (fltrlist)
16 CONTAINS_OP 0
18 POP_JUMP_IF_FALSE 2 (to 4)
20 LOAD_FAST 1 (k)
22 LOAD_FAST 2 (v)
24 MAP_ADD 2
26 JUMP_ABSOLUTE 2 (to 4)
>> 28 RETURN_VALUE
The second part is the disassembly of the dictionary comprehension. You may notice that within the
dictionary comprehension, python is trying to load fltrlist
from the global scope. The bug becomes
aparent because the variable is defined within the function scope or the local scope and is not
in the global scope.
Will you ever encounter this bug? Maybe not. But it was fun learning how the Python debugger worked.