1

I currently have a couple of classes following the pattern described in the code below:

from typing import NamedTuple

class Data(NamedTuple):
   name: str,
   value: float
   
   def json(self):
       return {'name': self.name, 'value': self.value}
    

Instead of defining a json method in every new class that I create, I would like to extend typing.NamedTuple, so that I can extend from my new class without having to define the json method in the subclasses. typing.NamedTuple already provides the _asdict method, but this method is not sufficient because it is not recursive (Nested namedtuples will not be converted to dict objects).

I have tried to attempt this in the code below:

class JsonNamedTuple(NamedTuple):
    def json(self):
        # some slightly complex recursive code here

class Data(JsonNamedTuple):
    name: str,
    value: float

a = Data('asdf', 0.5)

I get this error: TypeError: <lambda>() takes 1 positional argument but 3 were given

Is there a way to extend typing.NamedTuple without getting this error?

Jadiel de Armas
  • 8,405
  • 7
  • 46
  • 62
  • 1
    On a terminological note, `json` doesn't return JSON; it returns a `dict`. – chepner Apr 06 '22 at 19:30
  • 3
    Named tuples already have a method that returns such a dict: `Data('asdf', 0.5)._asdict()`. – chepner Apr 06 '22 at 19:33
  • Because of how `NamedTuple` works, I think you have to provide the fields immediately; you cannot "suspect" the creation of the new type until a subclass provides fields. – chepner Apr 06 '22 at 19:38
  • @chepner `_asdict()` helps, but is not recursive. If I have a `NamedTuple` nested inside a `NamedTuple`, when calling `_asdict()` on the outer object will still return a `NamedTuple` in the inner one. – Jadiel de Armas Apr 06 '22 at 19:44
  • 1
    That's a requirement that should be added to the question. – chepner Apr 06 '22 at 19:53
  • (Oh, and "suspend", not "suspect".) – chepner Apr 06 '22 at 19:57
  • Note, typing.NamedTuple isn't a real class. So extending it isn't going to work. Note, `isinstance(Data('foo', 4.24), NamedTuple)` is False. See https://stackoverflow.com/questions/60707607/weird-mro-result-when-inheriting-directly-from-typing-namedtuple – juanpa.arrivillaga Apr 06 '22 at 20:32

1 Answers1

1

Unfortunately, you can't actually extend typing.NamedTuple.

You can however create a class decorator like so:

import typing

class _JsonMixin:
    def json(self):
        # some slightly complex recursive code here
        return {'name': self.name, 'value': self.value}

def jsonable(cls):
    cls.__bases__ = (_JsonMixin,) + cls.__bases__
    return cls

@jsonable
class Data(typing.NamedTuple):
    name: str
    value: float

Do mind type checkers will not understand where the .json() come from. Modifying the __bases__ is preferable to creating a new class and inheriting from cls, and easily supports more methods unlike injecting the .json().

If you don't wish to use a class decorator you can create a metaclass, but that's a bit more complicated. A metaclass won't work with the type checkers either.

Bharel
  • 23,672
  • 5
  • 40
  • 80