The nightly report
used to be one big job.
You split it up,
one job per region,
so a slow region
would not hold up the rest.
Three regions.
North, south, west.
A loop,
and inside it
a small lambda
handed to the scheduler.
for region in regions,
add a job
that builds the report for region.
It looked right.
It read right.
At six in the morning
three reports arrived.
All three said west.
North and south
got the west numbers,
with their own names
in the subject line.
Nothing crashed.
The scheduler did exactly
what it was given.
The lambda did not keep
the region it was born with.
It kept the name.
Region.
A name that lives
in the scope around it,
and is looked up
when the function runs,
not when it was made.
By the time the scheduler ran anything,
the loop had long finished,
and region meant west,
for everyone.
It is called late binding,
and Python is not alone.
JavaScript did the same
with var in a loop,
until let gave each turn
its own variable.
Python has no let.
A list comprehension
does not save you either.
I ran it both ways.
Three lambdas,
three times the word west.
Your tests did not see it
because they called the function
straight away,
inside the loop,
while region still meant
the region they expected.
The bug only exists
when there is a gap
between making the function
and calling it.
And a gap
is the whole point
of a scheduler,
a callback,
a button handler,
a retry.
So bind the value
at the moment you mean it.
Use functools.partial,
which stores the argument itself,
not the name.
Or write a small function
that takes the region
and returns the job,
so each call
gets a fresh scope to keep.
And when a loop
hands functions to something
that will call them later,
test it later.
Collect the jobs first.
Run them after the loop.
Check each one
did its own work.
A closure does not remember
what a variable was.
It remembers where to look.
– Serguey Asael Shinder


