Python for scoping quirk
I’ve come across a bug/quirk/annoyance/pitfall of the scoping of Python’s for
.
In this code:
names = ['Alice', 'Bob']
for i, name in enumerate(names):
print(f'{i} - {name}')
print(f'Printed {i} names')
i
is in scope at the final print
. This alone may constitue a surprise (it did for me the first time) and can lead to shadow bugs:
name = 'Andrew'
names = ['Alice', 'Bob']
for name in names:
print(name)
print(name)
Obviously contrived, but the last print
prints Bob
and not Andrew
.
Back to our original example, this seems like a nice pattern because i
will contain the number of loop iterations minus 1. This is nice if names
were a generator and not a list.
But now the pain is real when your code tries to enumerate an empty list/generator/iterable:
names = []
for i, name in enumerate(names):
print(f'{i} - {name}')
print(f'Printed {i} names')
Errors out with NameError: name 'i' is not defined
. Ouch!
You might expect i
to be 0
in this case, but remember i
will be # loops minus 1 so it would actually have to be -1
for that to be true.
Going forward I’ll probably stick to len
on things which support it and using an explicit counter on general iterators. An explicit counter feels horrible, I know, but is less work (code & perf) than tee’ing and doing sum(1 for _ in iterator)