59

I accidentally wrote some code like this:

foo = [42]
k = {'c': 'd'}

for k['z'] in foo:  # Huh??
    print k

But to my surprise, this was not a syntax error. Instead, it prints {'c': 'd', 'z': 42}.

My guess is that the code is translated literally to something like:

i = iter(foo)
while True:
    try:
        k['z'] = i.next()  # literally translated to assignment; modifies k!
        print k
    except StopIteration:
        break

But... why is this allowed by the language? I would expect only single identifiers and tuples of identifiers should be allowed in the for-stmt's target expression. Is there any situation in which this is actually useful, not just a weird gotcha?

martineau
  • 119,623
  • 25
  • 170
  • 301
jtbandes
  • 115,675
  • 35
  • 233
  • 266
  • It doesn't have to be useful to be allowed. A keyed value in a dictionary is a valid identifier. Since namespaces are, themselves, dictionaries, changing this would require changing a lot of Python internals. And to fix something that is only an issue if you write accidental code that doesn't result in an error. – Alan Leuthard Jun 01 '17 at 23:32
  • @AlanLeuthard I see what you're saying (and your answer fleshes it out nicely) but to be more precise, an [`identifier`](https://docs.python.org/2/reference/lexical_analysis.html#grammar-token-identifier) can only be a sequence of letters and digits, i.e. an actual variable *name* rather than some other assignable expression such as a [`subscription`](https://docs.python.org/2/reference/expressions.html#grammar-token-subscription). – jtbandes Jun 02 '17 at 06:08
  • Yeah. Should have said target instead of identifier – Alan Leuthard Jun 02 '17 at 15:03
  • @JimFasarakisHilliard What do you think of making the other a duplicate of this one? The answers here are much more thorough. – jtbandes Jul 25 '17 at 17:17
  • 1
    Oh damn, I'm sorry, this was supposed to be the other way around :-) Shouldn't use the hammer when sleepy. – Dimitris Fasarakis Hilliard Jul 25 '17 at 17:19
  • Interesting how much more attention this got. Before posting I tried and failed to find any existing questions on this topic, but it's a bit hard to search for...so...good sleuthing! – jtbandes Jul 25 '17 at 17:21
  • 1
    Possible duplicate of [Are for-loop name list expressions legal?](https://stackoverflow.com/questions/39539606/are-for-loop-name-list-expressions-legal) – Uyghur Lives Matter Jul 25 '17 at 20:52

4 Answers4

32

The for loop follows the standard rules of assignment so what works on the LHS of a vanilla assignment should work with the for:

Each item in turn is assigned to the target list using the standard rules for assignments

The for construct simply summons the underlying mechanism for assigning to the target which in the case of your sample code is STORE_SUBSCR:

>>> foo = [42]
>>> k = {'c': 'd'}
>>> dis.dis('for k["e"] in foo: pass')
  1           0 SETUP_LOOP              16 (to 18)
              2 LOAD_NAME                0 (foo)
              4 GET_ITER
        >>    6 FOR_ITER                 8 (to 16)
              8 LOAD_NAME                1 (k)
             10 LOAD_CONST               0 ('e')
             12 STORE_SUBSCR <--------------------
             14 JUMP_ABSOLUTE            6
        >>   16 POP_BLOCK
        >>   18 LOAD_CONST               1 (None)
             20 RETURN_VALUE

But to my surprise, this was not a syntax error

Apparently, whatever works in a regular assignment such as the following:

full slice assignment:

>>> for [][:] in []:
...    pass
... 
>>>

list subscription

>>> for [2][0] in [42]:
...    pass
... 
>>> 

dictionary subscription etc. would be valid candidate targets, with the lone exception being a chained assignment; although, I secretly think one can cook up some dirty syntax to perform the chaining.


I would expect only single identifiers and tuples of identifiers

I can't think of a good use case for a dictionary key as a target. Besides, it is more readable to do the dictionary key assignment in the loop body, than use it as a target in the for clause.

However, extended unpacking (Python 3) which is very useful in regular assignments also comes equally handy in a for loop:

>>> lst = [[1, '', '', 3], [3, '', '', 6]]
>>> for x, *y, z in lst:
...    print(x,y,z)
... 
1 ['', ''] 3
3 ['', ''] 6

The corresponding mechanism for assigning to the different targets here is also summoned; multiple STORE_NAMEs:

>>> dis.dis('for x, *y, z in lst: pass')
  1           0 SETUP_LOOP              20 (to 22)
              2 LOAD_NAME                0 (lst)
              4 GET_ITER
        >>    6 FOR_ITER                12 (to 20)
              8 EXTENDED_ARG             1
             10 UNPACK_EX              257
             12 STORE_NAME               1 (x) <-----
             14 STORE_NAME               2 (y) <-----
             16 STORE_NAME               3 (z) <-----
             18 JUMP_ABSOLUTE            6
        >>   20 POP_BLOCK
        >>   22 LOAD_CONST               0 (None)
             24 RETURN_VALUE

Goes to show that a for is barely simple assignment statements executed successively.

Moses Koledoye
  • 77,341
  • 8
  • 133
  • 139
  • 2
    Given that chained assignment would be `(x = (y = v))` not `(x = y) = v`, I doubt you can come up with syntax to make it work – Bergi Jun 02 '17 at 05:53
28

The following code would make sense, right?

foo = [42]
for x in foo:
    print x

The for loop would iterate over the list foo and assign each object to the name x in the current namespace in turn. The result would be a single iteration and a single print of 42.

In place of x in your code, you have k['z']. k['z'] is a valid storage name. Like x in my example, it doesn't yet exist. It is, in effect, k.z in the global namespace. The loop creates k.z or k['z'] and assigns the the values it finds in foo to it in the same way it would create x and assign the values to it in my example. If you had more values in foo...

foo = [42, 51, "bill", "ted"]
k = {'c': 'd'}
for k['z'] in foo:
    print k

would result in:

{'c': 'd', 'z': 42}
{'c': 'd', 'z': 51}
{'c': 'd', 'z': 'bill'}
{'c': 'd', 'z': 'ted'}

You wrote perfectly valid accidental code. It's not even strange code. You just usually don't think of dictionary entries as variables.

Even if the code isn't strange, how can allowing such an assignment be useful?

key_list = ['home', 'car', 'bike', 'locker']
loc_list = ['under couch', 'on counter', 'in garage', 'in locker'] 
chain = {}
for index, chain[key_list[index]] in enumerate(loc_list):
    pass

Probably not the best way to do that, but puts two equal length lists together into a dictionary. I'm sure there are other things more experienced programmers have used dictionary key assignment in for loops for. Maybe...

GBrookman
  • 333
  • 1
  • 4
  • 13
Alan Leuthard
  • 798
  • 5
  • 11
  • 2
    This explanation made it much clearer than the rest. Good work! – tpg2114 Jun 02 '17 at 03:55
  • 1
    While your first code sample doesn't go too far beyond my "guess" in the question, this explanation is very clear and I'm sure it will be useful to future readers...if anyone ever happens to write this accidentally again :) Thanks! – jtbandes Jun 02 '17 at 06:06
  • 2
    Although `dict(zip(key_list, loc_list))` is probably easier to understand than this abuse of the `for` loop. – Graipher Jun 02 '17 at 06:13
  • “in effect, k.z in the global namespace”, why is that? – laike9m Jun 07 '17 at 14:02
  • That actually works better the other way around. Every name you see is basically namespace.name which is equivalent to namespace['name']. And every subname is namespace.name.subname or namespace['name']['subname']. In other words, every variable name is a dictionary key. A hash table is used to find a reference in memory storing the value. – Alan Leuthard Jun 08 '17 at 21:40
7

Every name is just a dictionary key*.

for x in blah:

is precisely

for vars()['x'] in blah:

* (though that dictionary needn't be implemented as an actual dict object, in case of some optimizations, such as in function scopes).

Veky
  • 2,646
  • 1
  • 21
  • 30
  • These two code examples aren't "precisely" the same, since the first one works inside functions while the second one doesn't, but your main point stands. – Sven Marnach Jun 02 '17 at 11:33
  • Maybe my English is the problem. I didn't say, nor have I intended to say, that those code samples are precisely the same. (Of course, since you've probably seen that I included the footnote below.) I meant just, "the precise semantics of assignment to a name is just a setitem on some dictionary", though that dictionary needn't be implemented as a real Python dict object. – Veky Jun 08 '17 at 16:01
  • 2
    Yeah, fair enough, as an illustration this makes total sense. I've just seen too many people being confused by the fact that modifying the dictionary returned by `locals()` (or `vars()`) does not work inside functions, so I prefer to be as explicit as possible about it. – Sven Marnach Jun 08 '17 at 18:41
5

Is there any situation in which this is actually useful?

Indeed. Ever wanted to get rid of itertools.combinations?

def combinations (pool, repeat):        
    def combinations_recurse (acc, pool, index = 0):
        if index < len(acc):
            for acc[index] in pool:
                yield from combinations_recurse(acc, pool, index + 1)
        else:
            yield acc

    yield from combinations_recurse([pool[0]] * repeat, pool)

for comb in combinations([0, 1], 3):
    print(comb)
Uriel
  • 15,579
  • 6
  • 25
  • 46