Search code examples
pythonchained-assignment

Chained assignment for mutable types


Ran into this issue when debugging a piece of code. If was not aware of this behaviour previously.

foo = bar = [1, 2, 3]

hex(id(foo))
Out[121]: '0x1f315dafe48'
hex(id(bar))
Out[122]: '0x1f315dafe48'

Both "variables" are pointing to the same memory location. But now if one is changed, the other changes as well:

foo.append(4)

bar
Out[126]: [1, 2, 3, 4]

So essentially here we have two names assigned to the same variable/memory address. This is different from:

foo = [1, 2, 3]
bar = [1, 2 ,3]
hex(id(foo))
Out[129]: '0x1f315198448'
hex(id(bar))
Out[130]: '0x1f319567dc8'

Here a change to either foo or bar won't have any effect on the other one.

So my question is: why does this feature (chained assignment for mutable types) even exist in Python? Does it serve any purpose apart from giving you tools to shoot yourself in the foot?


Solution

  • It's useful for simple, common initializations like

    foo = bar = baz = 0
    

    so you don't have to write

    foo = 0
    bar = 0
    baz = 0
    

    Since it's a syntax feature, it's not really feasible to make it only work for immutable types. The parser can't tell whether the expression at the end will be a mutable or immutable type. You can have

    def initial_value():
        if random.choice([True, False]):
            return []
        else:
            return 0
    
    foo = bar = baz = initial_value()
    

    initial_value() can return a mutable or immutable value. The parser for the assignment can't know what it will be.

    There are lots of ways to shoot yourself in the foot with multiple references to mutable values, Python doesn't go out of its way to stop you. For some of the more common examples, see "Least Astonishment" and the Mutable Default Argument and List of lists changes reflected across sublists unexpectedly

    You just have to remember that in a chained assignment, the value expression is only evaluated once. So your assignment is equivalent to

    temp = [1, 2, 3]
    foo = temp
    bar = temp
    

    rather than

    foo = [1, 2, 3]
    bar = [1, 2, 3]
    

    See How do chained assignments work?

    A more general rule to remember is that Python never makes copies of objects spontaneously, you always have to tell it to do so.