10

I'm trying to make a NamedTuple where one field defaults to an empty dictionary. This mostly works, however the default value is shared between instances of the NamedTuple:

from typing import NamedTuple, Dict

class MyTuple(NamedTuple):
    foo: int
    bar: Dict[str, str] = {}


t1 = MyTuple(1, {})
t2 = MyTuple(2)
t3 = MyTuple(3)

t2.bar["test2"] = "t2"
t3.bar["test3"] = "t3"

print(t2)  # MyTuple(foo=2, bar={'test2': 't2', 'test3': 't3'})
print(t3)  # MyTuple(foo=3, bar={'test2': 't2', 'test3': 't3'})
assert "test3" not in t2.bar  # raises

How can I make sure the bar field is a new dict for each instance? All of the examples of dicts in PEP-526 seem to use ClassVar, but that's the opposite of what I want here.

I could potentially use a dataclass here with a default factory function (or the equivalent in attrs), but I currently need to support python 3.6.x and 3.7.x, so that would add some overhead.

For what it's worth, the version of python where I'm testing this is 3.7.3

celion
  • 3,864
  • 25
  • 19
  • Dupe with [How do I add `datetime.now().isoformat()` to NamedTuple's `__init__`?](https://stackoverflow.com/q/52212631/674039) – wim May 23 '22 at 17:16

1 Answers1

6

typing.NamedTuple/collections.namedtuple don't support factory functions, and the mechanism that implements the defaults doesn't look to be overloadable in any reasonable way. Nor can you implement such a default manually by writing your own __new__ directly.

AFAICT, the only semi-reasonable way to do this is by writing a subclass of a namedtuple that implements its own __new__ that generates the default programmatically:

class MyTuple(NamedTuple):
    foo: int
    bar: Dict[str, str] = {}  # You can continue to declare the default, even though you never use it

class MyTuple(MyTuple):
    __slots__ = ()
    def __new__(cls, foo, bar=None):
        if bar is None:
            bar = {}
        return super().__new__(cls, foo, bar)

The overhead is relatively minimal, but it does involve half a dozen lines of boilerplate. You can reduce the boilerplate (at the expense of possibly over-dense code) one-line the definition of the parent MyTuple into the definition of the final MyTuple to reduce verbosity, e.g.:

class MyTuple(typing.NamedTuple('MyTuple', [('foo', int), ('bar', Dict[str, str])])):
    __slots__ = ()
    def __new__(cls, foo, bar=None):
        if bar is None:
            bar = {}
        return super().__new__(cls, foo, bar)

but that will still make an inheritance hierarchy, not merely a single direct descendant of the tuple class like "inheriting" from NamedTuple does. This shouldn't impact performance meaningfully, just something to be aware of.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271