Difference in packing with "()" and "[]"

Can someone say what’s the difference of these two approaches?

Using ( )
a, (b, c) = "a", "bc"

Using [ ]
a, [b, c] = "a", "bc"

Both the approaches are unpacking it in the same way. And random people in the internet say the first approach of using “( )” is faster in time because of tuple unpacking.
And personally I find using “[ ]” on the left hand side of the “=” looks so weird and not pythonic way of unpacking.

So is there any special difference of these two unpacking?

This does not seem to be the case.

Python 3.12.6 (main, Sep 10 2024, 00:05:17) [GCC 9.4.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.21.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: %timeit a, [b, c] = "a", "bc"
36.4 ns ± 0.533 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

In [2]: %timeit a, (b, c) = "a", "bc"
36.2 ns ± 0.112 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

Can you show us some of them? Maybe you misunderstood them.

The only difference is in the parse tree generated.

>>> x = 'a, (b, c) = "a", "bc"'
>>> y = 'a, [b, c] = "a", "bc"'
>>> print(ast.dump(ast.parse(x), indent=2))
Module(
  body=[
    Assign(
      targets=[
        Tuple(
          elts=[
            Name(id='a', ctx=Store()),
            Tuple(
              elts=[
                Name(id='b', ctx=Store()),
                Name(id='c', ctx=Store())],
              ctx=Store())],
          ctx=Store())],
      value=Tuple(
        elts=[
          Constant(value='a'),
          Constant(value='bc')],
        ctx=Load()))],
  type_ignores=[])
>>> print(ast.dump(ast.parse(y), indent=2))
Module(
  body=[
    Assign(
      targets=[
        Tuple(
          elts=[
            Name(id='a', ctx=Store()),
            List(
              elts=[
                Name(id='b', ctx=Store()),
                Name(id='c', ctx=Store())],
              ctx=Store())],
          ctx=Store())],
      value=Tuple(
        elts=[
          Constant(value='a'),
          Constant(value='bc')],
        ctx=Load()))],
  type_ignores=[])

The code generated from each parse tree is identical.

>>> import dis
>>> dis.dis(x)
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (('a', 'bc'))
              4 UNPACK_SEQUENCE          2
              8 STORE_NAME               0 (a)
             10 UNPACK_SEQUENCE          2
             14 STORE_NAME               1 (b)
             16 STORE_NAME               2 (c)
             18 RETURN_CONST             1 (None)
>>> dis.dis(y)
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (('a', 'bc'))
              4 UNPACK_SEQUENCE          2
              8 STORE_NAME               0 (a)
             10 UNPACK_SEQUENCE          2
             14 STORE_NAME               1 (b)
             16 STORE_NAME               2 (c)
             18 RETURN_CONST             1 (None)
>>>
6 Likes

I’ve heard this before when defining constants in files. The thought was use tuples since the allocation is faster since it can’t change sizes.

No idea if it’s actually true or where I got that from.

1 Like

@csm10495 But you mean actual tuple objects, right? Not just such “tuple syntax” in target lists.

It sounds to me like somewhere along the telephone chain there was a misgeneralization from “tuples are faster than lists"[1] to “sequence unpacking with parentheses is faster than sequence unpacking with brackets.” That isn’t the case. Both a, (b, c) = ... and a, [b, c] = ... are equivalent syntaxes for sequence unpacking that are compiled to the same bytecode.

What the original writer of that claim may have been thinking about is the fact that tuple construction is often faster than list construction, for a variety of reasons. This is actually a somewhat complicated bit of trivia, the details of which change a lot between CPython versions. But in short creating tuples is often faster than creating lists because

  1. more compile-time optimizations apply to tuple construction, especially in older versions of CPython.
  2. constructing a new tuple takes less work at runtime than constructing a new list.

More details can be found in this (slightly dated) SO answer: performance - Why is tuple faster than list in Python? - Stack Overflow

An implication of the above is that it is true that this:

a, b = (b, a)

is faster than this:

a, b = [b, a]

In the first case CPython optimizes away the tuple creation entirely and emits bytecode to directly swap a and b, while in the second case it resorts to building and unpacking an intermediate list[2].


  1. for some meanings of “faster”, in some circumstances ↩︎

  2. verified for CPython 3.{8,9,10,11,12,13} ↩︎

4 Likes

Yep exactly

This somewhat surprises me. Given that the code generator needs to verify that the number of elements in (b, a) equals the number of elements in a, b to generate the SWAP instruction, it doesn’t seem like it would take that much more work to identify [b, a] as an anonymous list that won’t be used beyond the unpacking operation. The parse tree would immediately distinguish a regular list display from a list comprehension, so it seem like a, b = [b, a] could easily be optimized just like a, b = (b, a).

2 Likes

I think this is just because it was manually optimized, not something inherent to tuples vs lists. I think the tuple version is much more common and so it was a target for optimization.

The list version could be optimized too but I a) doubt it would apply very often and b) a higher-level strategy (e.g. improving the JIT) is probably better ROI.

2 Likes