Example code to replicate issue on python 3.10.2 is attached.
How to replicate issue:
1. Define a namedtuple where all fields have default values. At least one field's default value will be an empty list: []
2. Instantiate 2 instances of the namedtuple.
3. In the first instance, use namedtuple._replace() to add a replacement list into a field that defaults to [].
4. Now look at the contents of both namedtuples. *Both* of them are modified, even though the code only modified the first object.
***
Output from attached example code:
g.P5 1: myNamedTuple(P1='hello', P2='world', P3=None, P4='', P5=[], P6=[])
h.P5 1: myNamedTuple(P1='good', P2='morning', P3=None, P4='', P5=[], P6=[])
Expected: g.P5: []
Actual: g.P5: []
Expected: h.P5: []
Actual: h.P5: []
g.P5 2: myNamedTuple(P1='hello', P2='world', P3=None, P4='', P5=['a', 'b'], P6=[])
h.P5 2: myNamedTuple(P1='good', P2='morning', P3=None, P4='', P5=['a', 'b'], P6=[])
Expected: g.P5: ['a', 'b']
Actual: g.P5: ['a', 'b']
Expected: h.P5: []
Actual: h.P5: ['a', 'b']
g.P5 3: myNamedTuple(P1='hello', P2='world', P3=None, P4='', P5=['a', 'b', 'c', 'd'], P6=[])
h.P5 3: myNamedTuple(P1='good', P2='morning', P3=None, P4='', P5=['a', 'b', 'c', 'd'], P6=[])
Expected: g.P5: ['a', 'b', 'c', 'd']
Actual: g.P5: ['a', 'b', 'c', 'd']
Expected: h.P5: []
Actual: h.P5: ['a', 'b', 'c', 'd']
|
This seems to be the standard confusion people have with mutable defaults, just with namedtuple defaults rather than function defaults.
Similar behavior elsewhere in Python:
>>> def f(x, y=[]):
... y.append(x)
... print(y)
...
...
>>> f(1)
[1]
>>> f(2)
[1, 2]
>>> f(15)
[1, 2, 15]
Or even
>>> a = []
>>> b = a
>>> b.append(4)
>>> a
[4]
This happens because the values you provide as defaults are *objects*, not *expressions* to be re-evaluated. If you make one of the defaults a list, you ask the namedtuple to "give me this particular list every time", and it will give you that same list, even if it has been modified.
There is currently no support for "construct a brand new list every time" in namedtuples. You could look to the dataclasses module for such a feature, where you can specify default_factory=list, and it will make a new list for each instance.
https://docs.python.org/3/library/dataclasses.html#dataclasses.field
|