Hacker News new | ask | show | jobs
by jmconfuzeus 1342 days ago
Technically, it makes sense for Python to work like this because your functions are being bound to a variable i which is located in the global scope.

I wouldn't call this a design mistake but more of a misunderstanding of how scope works.

1 comments

If other languages get this right then it's not a fundamental misunderstanding but a quirk of that specific language.
Dunno what you mean by getting it right tbh.

for i in range(10)

The statement literally declares or sets a variable named i in that scope. When the loop exits, i still exists in the scope with the value 9. If you call a function that was given a reference to i, the value will be 9 as expected because the function was called after the loop exited.

Don't see any quirk or mistake here.

An equivalent construct works differently (and one would say in a way that is less error-prone) in other languages.

For instance the behaviour of a Python loop varies drastically depending on the size of the iteration:

     def loop(n):
         for i in range(n):
             pass
         print(i)

     loop(10) # 9
     loop(0)
     Traceback (most recent call last):
       File "<stdin>", line 1, in <module>
       File "<stdin>", line 4, in loop
     UnboundLocalError: local variable 'i' referenced before assignment
That Python works this way is specific to Python. And a language which doesn't have the issues this implies would be "getting it right", in the sense of avoiding sharp corners and edge cases.
I see, it makes sense to force people to use different variables for iteration in that case because it can easily trip inexperienced developers up.
The statement can mean whatever the language designers decide it should mean, and one plausible meaning is to introduce a new scope for the body of the loop. It's not even something unique in Python, given that sequence comprehensions do just that; e.g. this prints 0,1,...,9:

   for f in (lambda: print(i) for i in range(0, 10)): f()
Unfortunately, Python is simply inconsistent in this regard. For example, list comprehensions leak the variable for back-compat reasons, so if you substitute (lambda: ...) with [lambda: ...] above, you'll get a bunch of 9s.

But, backwards compatibility aside, the language could change to make for-loops behave like sequence comprehensions wrt scoping.

> Unfortunately, Python is simply inconsistent in this regard. For example, list comprehensions leak the variable for back-compat reasons

No, they don’t. They did in Python 2—list comps were introduced in 2.0, genexps in 2.4, and set/dict comps in 3.0 but also included in the later 2.7 release—but that’s been non-current for more than a decade, and conpletely out of support for two years. Let it go.

> But, backwards compatibility aside, the language could change to make for-loops behave like sequence comprehensions wrt scoping.

Sure in Python 4, but after 2->3, not sure many people are looking forward to that.

   Python 3.10.5 (tags/v3.10.5:f377153, Jun  6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)] on win32
   Type "help", "copyright", "credits" or "license" for more information.
   >>> [f() for f in [lambda: i for i in range(0, 10)]]
   [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
   >>> [f() for f in (lambda: i for i in range(0, 10))]
   [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
I don't know what this is supposed to prove, it just shows that the generator is lazy while the list isn't. It seems unrelated to scoping issues or leaking variables.