classification
Title: More details in reference 'Looping through a list in Python and modifying it'
Type: enhancement Stage:
Components: Documentation Versions: Python 3.7, Python 3.6
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: rhettinger Nosy List: docs@python, r.david.murray, rhettinger, terry.reedy, the_darklord
Priority: normal Keywords:

Created on 2017-07-02 04:19 by the_darklord, last changed 2017-09-24 19:59 by terry.reedy.

Messages (8)
msg297507 - (view) Author: Anmol Gupta (the_darklord) Date: 2017-07-02 04:22
Documentation section: https://docs.python.org/3/reference/compound_stmts.html#for

The documentation does not explain at all why is there an infinite loop when not using a copy of the list.

It leaves the reader in a confused state.

Even there are questions concerning the same on stackoverlflow: https://stackoverflow.com/questions/44633798/loop-through-a-list-in-python-and-modify-it
msg297509 - (view) Author: Anmol Gupta (the_darklord) Date: 2017-07-02 06:29
Wrong documentaion section linked.

Correct seciton: Section 4.2 on https://docs.python.org/3/tutorial/controlflow.html

The last line needs more explanation.
msg297514 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2017-07-02 12:37
The example would be more clear if we replaced the opaque idiom "words[:]" with the more explicit alternative "words.copy()".
msg297556 - (view) Author: Anmol Gupta (the_darklord) Date: 2017-07-03 10:44
And also a small explanation for why there would be an infinite loop without creating a copy.
msg297925 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-07-08 02:47
Are you looking for something like:

Let it = iter(words).  When next(it) returns 'defenestrate', insertion at the beginning moves the original 'defenestrate' over so that next(words) returns 'defenestrate' again.
?
msg298214 - (view) Author: R. David Murray (r.david.murray) * (Python committer) Date: 2017-07-12 13:20
I don't think that helps.  The issue here is that *sequences* are iterated over by incrementing an integer index.  If you change the size of the list, you are potentially changing which value any given index points to.  Presumably the tutorial writer thought this was intuitive, and indeed after years of Python programming I find it so.  I can see how a beginner might not, though :)

What if we replaced:

  If you need to modify the sequence you are iterating over while inside the loop (for example to duplicate selected items), it is recommended that you first make a copy. Iterating over a sequence does not implicitly make a copy. The slice notation makes this especially convenient:

With:

  Sequence iteration is preformed by incrementing an implicit integer index until there are no more items in the sequence.  The sequence is *not* copied before iteration, so if you modify the sequence during iteration the value that is affected by the next iteration of the loop may not be the one you are expecting.  You can avoid this problem by iterating over a copy of the sequence, and the slice notation makes this especially convenient:

However, this section has a deeper problem.  It is introducing the 'for' statement, but explains what the for statement does in terms of sequences, when in fact the for statement now operates on any iterable, not just sequences.  (Many Python programmers probably do not remember the time before the iteration protocol was added to the language :)

Fixing that problem not only requires rewriting the section, but also figuring out the best place to introduce the concept of the iteration protocol (which *might* be in this section, but it's been so long since I've looked over the tutorial that I can't say).
msg302874 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2017-09-24 18:00
After looking at this again, I think the entire example should be removed.  We really don't want to encourage people to code like this (it would never make it through a code review).  The example itself is silly (not fully, just weird and lacking real-world motiviation).  The s.insert(0,x) code is an anti-pattern.  And in general, mutating a data structure while iterating over it is a perilous practice leading to fragile code (many data structures ban the practice outright: databases, deques, dicts).

Mutating while iterating is only safe if a data structure makes explicit guarantees about how it iterates.  In Python, we have only a handful of such guarantees (you can safely mutate dict values while iterating over the keys and lists guarantee that the iterator looks-up consecutive indicies regardless of changes to the underlying list).

I propose to remove the last two paragraphs and the example, replacing them with clear practical advice and patterns that would pass a code review.

Something like this:

    Code that modifies a collection while iterating over
    that same collection can be tricky to get right.  Instead,
    it is usually more straight-forward to loop over a copy
    of the collection or to create a new collection.

    # Strategy:  Iterate over a copy
    for user, status in users.copy():
        if status == 'inactive':
            del users[user]

    # Strategy:  Create a new collection
    active_users = {}
    for user, status in users.items():
        if status == 'active':
            active_users[user] = status
msg302884 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-09-24 19:59
I agree that the tutorial For section needs be updated to include non-sequences. A dict example will help with that.

I agree that the unrealistic insert mis-directs attention and like Raymond's replacement.  ['users.copy()' should be 'users.copy().items']
History
Date User Action Args
2017-09-24 19:59:24terry.reedysetmessages: + msg302884
versions: + Python 3.7
2017-09-24 18:00:27rhettingersetmessages: + msg302874
2017-07-12 13:20:33r.david.murraysetnosy: + r.david.murray
messages: + msg298214
2017-07-08 02:47:20terry.reedysetnosy: + terry.reedy
messages: + msg297925
2017-07-03 10:44:14the_darklordsetmessages: + msg297556
2017-07-02 12:37:06rhettingersetassignee: docs@python -> rhettinger

messages: + msg297514
nosy: + rhettinger
2017-07-02 06:29:34the_darklordsetnosy: docs@python, the_darklord
messages: + msg297509
2017-07-02 04:22:08the_darklordsetmessages: + msg297507
2017-07-02 04:19:59the_darklordcreate