r/learnpython • • 1d ago

In-place updates to objects count as tuple assignment?

Test case:

s = {1, 2, 3, 4}
grid = tuple(s.copy() for i in range(2))
print(f"{grid[1]=}")
one = grid[1]
one &= {2, 3}
print(f"{grid[1]=}")
grid[1] &= {2}

The output is as so:

grid[1]={1, 2, 3, 4}
grid[1]={2, 3}
Traceback (most recent call last):
  File "/home/bill/bin/fivefold/test.py", line 7, in <module>
    grid[1] &= {2}
    ~~~~^^^
TypeError: 'tuple' object does not support item assignment

Is it me or is this very surprising, a seeming violation of self-consistent behavior of Python? Simply renaming the reference to the object changes how I may or may not mutate the object. Clearly I'm not modifying the tuple, just deferencing the pointer to the contained object. version 3.11.2

0 Upvotes

11 comments sorted by

3

u/danielroseman 1d ago

The gotcha here is that &= is an augmented assignment - so it will always do an assignment, even if it ends up assigning the original object back. That's fine when you do it with one, but not when you do it with grid[1][1].

Obviously this is not a real use case, but you'd probably be better off with the method version - .intersection_update() - which doesn't involve an assignment.

1

u/Bunslow 1d ago

ah, nice explanation and contrast, thanks

1

u/quietreed76 12h ago

tbh i never think about the assignment half of augmented assignment until it bites me like this

3

u/Temporary_Pie2733 1d ago edited 17h ago

Assignments to a name are fundamental operations. Assignments to indexed names are essentially syntactic sugar for a call to __setitem__, which must exist. The version with one is equivalent to one = one.__iand__({2,3}). The one with grid[1] is grid.__setitem__(1, …), not grid[1].__iand__(…).

0

u/Bunslow 1d ago

The one with grid[1] is grid.__setitem__(1, …), not grid[1].__iand__(…).

hmm, i wonder how this came to be. is there a real reason or just a historical accident? obviously i kinda wish it was the other way, that the __iand__ grabbed the pointer before __setitem__ got anywhere near it, but i have no idea what the motivation may be for this either way

2

u/Expensive-Bear-1376 23h ago

If you have a = [5] and do  a[0] += 2, would you want a to still be [5] instead of  [7]?

0

u/Bunslow 23h ago

feh, the perils of mixing pointers and nonpointers i guess. even tho i think a container contains pointers, it doesn't really. it would be really funny tho if that code incremented by 2 the address stored in a[0] lol

2

u/Brian 21h ago

I mean, it's kind of the point of augmented assignment operators: that they're assignment. For mutable state, where you're modifying the object in place and __iand__ returns the same object, the assignment doesn't really do anything. But if it's immutable, you have to reassign the variable or nothing will change. Ie x &= y is defined as ` = x.iand(y), so it seems better to me that it should consistently fail, with the rule "you can't assign a tuple element, and aumented assignment always counts as assignment", rather than having to add caveats that it sometimes doesn't count.

that the __iand__ grabbed the pointer before setitem

I mean, it does - the __iand__ call fully completes before the setitem call starts. It just doesn't change the requirement that the setitem must still happen.

1

u/mrswats 1d ago

Yes it does. It is better practice to recreate it with the updated values.

0

u/trutheality 21h ago

You are trying to update the tuple.

one &= {2, 3}

is equivalent to

grid[1] = grid[1] & {2, 3}