I suspect this may have been an accident, though I prefer the new behavior.
The new behavior is a consequence of a change to how the bytecode for *
arguments works. The change is in the changelog under Python 3.9.0 alpha 3:
bpo-39320: Replace four complex bytecodes for building sequences with three simpler ones.
The following four bytecodes have been removed:
- BUILD_LIST_UNPACK
- BUILD_TUPLE_UNPACK
- BUILD_SET_UNPACK
- BUILD_TUPLE_UNPACK_WITH_CALL
The following three bytecodes have been added:
- LIST_TO_TUPLE
- LIST_EXTEND
- SET_UPDATE
On Python 3.8, the bytecode for f(*a, a.pop())
looks like this:
1 0 LOAD_NAME 0 (f)
2 LOAD_NAME 1 (a)
4 LOAD_NAME 1 (a)
6 LOAD_METHOD 2 (pop)
8 CALL_METHOD 0
10 BUILD_TUPLE 1
12 BUILD_TUPLE_UNPACK_WITH_CALL 2
14 CALL_FUNCTION_EX 0
16 RETURN_VALUE
while on 3.9, it looks like this:
1 0 LOAD_NAME 0 (f)
2 BUILD_LIST 0
4 LOAD_NAME 1 (a)
6 LIST_EXTEND 1
8 LOAD_NAME 1 (a)
10 LOAD_METHOD 2 (pop)
12 CALL_METHOD 0
14 LIST_APPEND 1
16 LIST_TO_TUPLE
18 CALL_FUNCTION_EX 0
20 RETURN_VALUE
In the old bytecode, the code pushes a
and (a.pop(),)
onto the stack, then unpacks those two iterables into a tuple. In the new bytecode, the code pushes a list onto the stack, then does l.extend(a)
and l.append(a.pop())
, then calls tuple(l)
.
This change has the effect of shifting the unpacking of a
to before the pop
call, but this doesn’t seem to have been deliberate. Looking at bpo-39320, the intent was to simplify the bytecode instructions, not to change the behavior, and the bpo thread has no discussion of behavior changes.