Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typing.NamedTuple with a dictionary default

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

like image 450
celion Avatar asked Apr 08 '26 13:04

celion


1 Answers

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.

like image 139
ShadowRanger Avatar answered Apr 10 '26 01:04

ShadowRanger