Search code examples
python-3.xnamedtuple

provide additional constraints when declaring typing.NamedTuple object


Using a typing.NamedTuple object, what is the best way to enforce additional constraints on how it can be declared?

Let's say I have a Undergraduate class where the students have have a major but I want to enforce that 'undeclared' is an unacceptable value for the major.

from typing import NamedTuple

class Undergraduate(NamedTuple):
    name: str
    major: str

    def check_major(self):
        if self.major == "undeclared":
            raise ValueError("must declare a major")

if __name__ == "__main__":
    u1 = Undergraduate("Jane", "computer science") # no errors
    u1.check_major() # no errors
    u2 = Undergraduate("John", "undeclared") # no errors
    u2.check_major() # ValueError

This works fine but I would like for check_major() to run every time I declare a new object, ie:

u1 = Undergraduate("John", "undeclared") # immediate ValueError raised

Is this possible using only a NamedTuple (I know how to do it using traditional classes)?

Note: I read this related question. These solutions provide somewhat of a working solution, but like the OP I want to be able to instantiate the objects without requiring additional class methods to be called.


Solution

  • NamedTuple protects both __init__ and __new__ from being replaced at declaration. However, they can be replaced after the class was created.

    class Radial2D(NamedTuple):
          angle: float
          length: float
    
          def _verify_attributes_(self, *args):
              if self.length < 0 or not 0 < self.angle < 360:
                 raise ValueError('Arguments out of range')
    
    Radial2D.__init__ = Radial2D._verify_attributes_
    print(Radial2D(90, 15.5))  # Radial2D(angle=90, length=15.5)
    print(Radial2D(12, -5))    # ValueError: Arguments out of range
    

    This pattern can be simplified using a class decorator:

    from typing import Type, NamedTuple
    
    
    def verify(tp: Type[NamedTuple]):
        verifier = tp._verify_attributes_
        tp.__init__ = verifier
        return tp
    
    
    @verify
    class Undergraduate(NamedTuple):
        name: str
        major: str
    
        def _verify_attributes_(self, *args):
            if self.major == "undeclared":
                raise ValueError("must declare a major")
    
    
    print(Undergraduate("Jane", "computer science"))  # Undergraduate(name='Jane', major='computer science')
    print(Undergraduate("John", "undeclared"))        # ValueError: must declare a major