Pythonloopslistsmutability

Python for Loop Variable Does Not Change the List: Fixes

A Python for loop variable is a separate name, not a slot in the list, so assigning to it changes nothing and raises no error. The article separates rebinding a name from mutating an object, and shows why a nested list can be changed in the same loop while a number cannot. Three working fixes are included: index writes, comprehensions and slice assignment.

9 min readReferencePython · loops · lists · mutability · beginners

A Python for loop variable does not change the list because it is a separate name, not a slot inside the list: price = price * 2 rebinds that name to a new number and never touches prices. To change the list you must write into it — by index with enumerate, by rebuilding it with a comprehension, or with a slice assignment prices[:] = ....

Here is the code everyone arrives with:

prices = [120, 250, 90]
for price in prices:
    price = price * 2
print(prices)
[120, 250, 90]

No exception, no traceback, the program ran fine — and did nothing. Python did exactly what was written; what was written just wasn't what you meant.

Why does assigning to the loop variable do nothing?

Because for price in prices does not hand you a slot in the list. On every iteration Python takes the next element and binds the name price to it, exactly as if you had written price = prices[0], then price = prices[1]. After that, the name and the list slot are independent: price = price * 2 only changes what the name points at, and the list knows nothing about that name.

Two lines prove it: the name outlives the loop and still holds the last element.

prices = [120, 250, 90]
for price in prices:
    pass
print(price)
90

If price were a window into the list, it would not survive the loop. It does — it is an ordinary local name.

A loop variable is a new name, not a list slot

Saying "assignment does not work in a Python for loop" describes the symptom accurately but explains it backwards. Assignment works, and it works flawlessly: it binds the name on the left to the value on the right. The name on the left is simply not part of the list.

In Python a name is a label stuck onto an object. prices is a label on a list object; price is a label on the number object that currently sits in that list. When you write price = price * 2, Python builds the new number 240 and moves the price label onto it. The contents of the list are none of its business — slot zero still holds 120.

That is why the changes are not kept: Python writes them into a name, not into the list. Every iteration creates a fresh value and drops it when the next one starts.

So "a Python for loop variable does not change the list" is not a bug in the language; it is the difference between a name and an object.

Rebinding a name is not mutating an object

Rebinding (name = ...) touches only the name. Mutation (obj[i] = ..., obj.append(...), obj.sort()) touches the object itself, and every name pointing at that object sees the change.

Line inside the loop What actually happens The outer list
price = price * 2 the name price is rebound to a new number unchanged
price += 100 for numbers this is just price = price + 100 unchanged
prices[i] = price * 2 writes into a list slot by index changed
row.append(0) a method mutates the list object changed
row = row + [0] builds a new list, rebinds the name unchanged
row += [0] for a list, += extends in place changed

The last two rows read like synonyms and behave differently: for numbers += really does rebuild, for a list it extends the existing object.

Why can I change a nested list in the same loop, but not a number?

Because the element types behave differently. A number is immutable: all you can do is compute a new number and bind a name to it. A list is mutable: it has methods that edit its contents in place, and the loop variable points at the very same object stored in the outer list. So row.append(0) is visible from outside while price = price * 2 is not — same loop, opposite outcomes.

rows = [[1, 2], [3, 4]]
for row in rows:
    row.append(0)
print(rows)
[[1, 2, 0], [3, 4, 0]]

Now the same loop with a rebinding instead:

rows = [[1, 2], [3, 4]]
for row in rows:
    row = row + [0]
print(rows)
[[1, 2], [3, 4]]

One character of difference, opposite results — which is why the behaviour looks arbitrary until you separate names from objects.

The same rule explains why looping over a list of dicts from JSON edits the data with no tricks at all: a dict is mutable, so item["total"] = ... inside for item in items reaches the list itself. Indexing the outer list by field name, though — items["price"] instead of items[0]["price"] — is a different error entirely: list indices must be integers or slices, not str.

Mutability is also why a list cannot go into a set or be a dict key. There it fails loudly, as TypeError: unhashable type: 'list'; here it fails silently.

How do I modify list elements in a loop correctly?

There are three working approaches, and they differ not in style but in whether the list stays the same object. Writing by index and slice assignment edit the existing list; a comprehension builds a new one and hangs the old name on it. If something else references the list — say it was passed into a function — that difference decides whether your change survives.

Option 1: write by index with enumerate

prices = [120, 250, 90]
for i, price in enumerate(prices):
    prices[i] = price * 2
print(prices)
[240, 500, 180]

enumerate(prices) yields an index/element pair per iteration. The index is the missing slot: prices[i] = ... writes into the list, not into a name. See enumerate; without it you write for i in range(len(prices)) — correct, just noisier.

Drill the index habit on Where the capitals are: it asks for the positions of uppercase letters, not the characters.

Option 2: rebuild the list with a comprehension

prices = [120, 250, 90]
prices = [price * 2 for price in prices]
print(prices)
[240, 500, 180]

The most readable option when one rule applies to every element. Nothing magical: the right side builds a new list, the left side rebinds prices to it. Forms are collected under Comprehensions. Only non-decreasing is a pure rebuild exercise: keep each element that is not smaller than the previous one.

Option 3: slice assignment, when the object must stay the same

cart = [120, 250, 90]
backup = cart
cart[:] = [price * 2 for price in cart]
print(backup)
[240, 500, 180]

cart[:] = ... replaces the contents of the existing list instead of creating a new one, so backup — a second name for the same object — sees the change. Compare with a plain assignment:

cart = [120, 250, 90]
backup = cart
cart = [price * 2 for price in cart]
print(backup)
[120, 250, 90]
Approach Code Same list object Use when
Write by index for i, x in enumerate(a): a[i] = f(x) yes you need if, continue or neighbours
Comprehension a = [f(x) for x in a] no, a new list the ordinary case: one rule for every element
Slice assignment a[:] = [f(x) for x in a] yes something else references the list

Why does append() work inside a loop when assignment doesn't?

Because append() is a method that mutates the list object, while assignment is an operation on a name. Collecting results into a separate accumulator list means calling a method on an object, so the result stays in the object. There is no contradiction with the earlier examples: whatever touches the object works, whatever touches only a name does not.

result = []
for price in [120, 250, 90]:
    result.append(price * 2)
print(result)
[240, 500, 180]

That is a fourth working approach, often the clearest for beginners: leave the source list alone, build the answer next to it.

Strings and numbers are immutable, so the same rule bites differently

With strings the trap is more convincing, because a method call feels like it changes something.

words = ["bread", "milk"]
for word in words:
    word = word.upper()
print(words)
['bread', 'milk']

word.upper() does not change the string, it returns a new one. Strings are immutable: every "modifying" method builds a different object, which is bound to word and lost on the next iteration. Fix it the same way: words[i] = word.upper() with enumerate, or words = [w.upper() for w in words]. A tuple goes further — no append, no item assignment, so point[0] = 999 raises TypeError: 'tuple' object does not support item assignment. At least the tuple complains; the list and the number stay quiet.

Why does the same problem break functions that receive a list?

Because a function parameter is also just a name. Calling double_all(cart) binds the parameter prices to the same object cart points at. Writing prices = [...] inside the function rebinds that local name to a new list, and the caller's cart is untouched — the same mistake as with the loop variable, one level up.

def double_all(prices):
    prices = [p * 2 for p in prices]

cart = [120, 250, 90]
double_all(cart)
print(cart)
[120, 250, 90]

Slice assignment fixes it with a single edit:

def double_all(prices):
    prices[:] = [p * 2 for p in prices]

cart = [120, 250, 90]
double_all(cart)
print(cart)
[240, 500, 180]

The other honest option is to return a new list and assign it at the call site: cart = double_all(cart) — preferable when the function has no business mutating the caller's data.

Common mistakes

  1. Adding enumerate but still assigning to the variable. for i, price in enumerate(prices): price = price * 2 changes nothing — the index was obtained and never used. Write prices[i] = price * 2.
  2. Assuming += is always in place. row += [0] extends a list object; price += 100 builds a new number. Same syntax, different consequences.
  3. Assigning a new list inside a function. prices = [...] is invisible to the caller. Use prices[:] = ... or return the result.
  4. Treating backup = prices as a copy. It is a second name for one object, so prices[0] = 999 changes backup too. A real copy is prices.copy() or prices[:] — see Shallow and deep list copies.
  5. Building a grid as [row] * 3. Three references to one list, so grid[0][0] = 5 prints [[5, 0], [5, 0], [5, 0]]. Use [[0, 0] for _ in range(3)].
  6. Deleting elements while iterating. A different bug with a different mechanism: the loop walks an index while the list shrinks, so items get skipped. See deleting from a list while looping.
  7. Expecting a loop over dict.items() to update the dict. count = count * 10 changes nothing there; only totals[name] = count * 10 does.

In short

  • for x in a binds x to each element; x is an ordinary name, not a list slot.
  • Assignment rebinds a name, mutation edits an object — that decides whether the list sees the change.
  • Numbers and strings are immutable, so with them only rebuilding the list works.
  • A nested list is mutable in the same loop precisely because lists have in-place methods.
  • Three fixes: a[i] = ... via enumerate, a = [f(x) for x in a], a[:] = [f(x) for x in a].
  • Slice assignment alone keeps the same object — use it when the list arrived as a function argument.
  • Inside a function, prices = [...] never affects the caller; use prices[:] = ... or return.

Practice on real tasks

Solve tasks in the Python trainer with instant grading and hints.

Open trainer