Learn to Code via Tutorials on Repl.it!

← Back to all posts
A Strange Python "Bug", and How to Avoid It
fluffyyboii (3)

Introduction

Pull up the attached code!

With all the great things about Python people are talking about, it can feel like there just aren't any nuances to it. Of course, that's wrong! This code can appear to be a "bug" at first glance, but it actually has to do with how Python handles scoping, which has to do with whether or not specific functions or processes can access variables and how they actually see them.

What's wrong?

Assuming you've looked at the code I've attached, you're probably wondering, "Why does the first method work but the second doesn't?" Well, to understand why the code behaves like this, let's run through the steps the naïve method takes:
fs = []
Easy. Just make an empty list.
for i in range(5):
Repeat the next line five times, using i as a counter.
fs.append(lambda: print(i))
What does lambda mean? Well, it's a lambda expression, and it basically allows you to use functions without giving them a name. So when we call it, it should print the variable i. A bit tricky, but simple enough.
for f in fs:
For every function f in the list...
f()
...call f with no arguments.
Everything seems fine, so why doesn't it work? Well, let's see what happens when we call the first function, or fs[0]. Remember that we've defined the function to simply print(i), which seems to be what we want, right? Wrong! Let's break this down even further:

  • Look for a variable called i.
    • If it exists, move on.
    • If it doesn't, raise an error.
  • Print i.

The issue arises when we look for i: because we never assigned i to anything else (why would we?), and 4 was the last value i had in the loop (since 4 is the greatest number less than 5, I hope you knew that!), when Python looks for i, it will think we want the number 4. And since every function is defined like this, they all print the number 4!
So now we know why the naïve method doesn't work, but why does the first method work?

What's right?

The first method is exactly the same as the naïve one, but one line of code is different:

fs = []
for i in range(5):
  fs.append((lambda x: lambda: print(x))(i))   # this one!
for f in fs:
  f()

Now, we can focus solely on the one part that's changed:
(lambda x: lambda: print(x))(i)
What could this mean? Well, first we see lambda x:, which means we're making a function that takes a single argument (x), and does something with it. What does this function return? Well, it actually returns another function, in the form of another lambda expression. This function prints the parameter x, which was used in the function before it.

Now, notice how the outer function is in parentheses. This is because we call the function, passing i as the argument for x, and we add that to the list. The result is the function that prints x. What's the point of doing this? It has to do with the difference between local variables and global variables.

Remember that the reason the naïve method didn't work is because the variable i changed after we made the function. However, with this method, although i still changes, we're not using i -- we're using x, and x does not change! In this case, i is a global variable, since anything can access it, and x is a local variable, since only the outer function can see it. (These both have their own exceptions if you look hard enough, but that's not important!)

Whether or not a variable is local or global is called its scope. It's why this code doesn't work...

def make_nine():
  nine = 9
make_nine()
print(nine)

...because nine was declared locally, not globally. (If you're wondering, you can make nine global by adding global nine at the start of the make_nine function.)

Conclusion

This code demonstrates how scoping can impact how a program behaves in its logic. Most of the time, you won't need to worry about this too much. However, in cases like these, it's important to see this and know what could be going wrong. If you don't know how to catch these kinds of issues, they can be really hard to debug! So, when you're writing code and something strange like this happens, make sure your variables are in the proper scope!

Comments
hotnewtop
DynamicSquid (4915)

This is a really interesting and good tutorial!!

fluffyyboii (3)

This is my first time making a tutorial like this, any feedback would be appreciated!

XanderEhlert (152)

@fluffyyboii larger spacing would help it to be easier to read (on the post) other than that, great job!

fluffyyboii (3)

@XanderEhlert Thank you! I can adjust the spacing.