pythonpython-typingtypeddict

Prevent TypedDict from accepting arbitrary parameters


I noticed that TypedDict seems to let you pass any arguments to it which is not great.

class X(TypedDict):
    id: int

obj1 = X(id=4)
print(obj1)
# {'obj1': 1}

obj2 = X(id=4, thing=3)
print(obj2)
# {'obj1': 1, 'thing': 3} # bad!

I guess this is since TypedDict only works at the type checker level.

But if I still wanted to prevent this happening during runtime, what is the alternative to using a TypedDict?


Solution

  • Type safety in current versions of Python is not achieved at runtime, but through the use of a static ahead-of-execution analysis with mypy

    This is true also for dataclasses, which have a similar scope as TypedDict, with the difference that dataclasses will check for undefined attributes, but it would not really behave like a dict. This would be true for NamedTuples too (except that the object is immutable).

    If you want to enforce type safety at runtime, this must be done explicitly, e.g.:

    class Foo:
       def __init__(self, *, bar):
           if isinstance(bar, int):
               self.bar = bar
           else:
               raise TypeError
    
    Foo(bar=1)
    # <__main__.Foo at 0x7f5400f5c730>
    
    Foo(bar="1")
    # TypeError
    
    Foo(baz=1)
    # TypeError
    

    or defining a class that would be closer to a TypedDict, but with runtime type checking, you could do something like:

    class RuntimeTypedDict(dict):       
        def __init__(self, **kws):
            unseen = set(self.__annotations__.keys())
            for key, value in kws.items():
                # invalid key/value type checks replicated here for performance
                if key in self.__annotations__:
                    if isinstance(value, self.__annotations__[key]):
                        unseen.remove(key)
                    else:
                        raise TypeError("Invalid value type.")
                else:
                    raise TypeError("Invalid key.")
            if unseen != set():
                raise TypeError("Missing required key.")
            super(RuntimeTypedDict, self).__init__(**kws)
            
        def __setitem__(self, key, value):
            if key in self.__annotations__:
                if isinstance(value, self.__annotations__[key]):
                    super(RuntimeTypedDict, self).__setitem__(key, value)
                else:
                    raise TypeError("Invalid value type.")
            else:
                raise TypeError("Invalid key.")
    

    which can be used similarly to TypedDict:

    class MyDict(RuntimeTypedDict):
        # __annotations__ = {"x": int}  # use this on older Python versions
        x: int
    
    
    d = MyDict(x=1)
    print(d)
    # {'x': 1}
    
    d["x"] = 2
    print(d)
    # {'x': 2}
    
    d["x"] = 1.1
    # TypeError: Invalid value type.
    d["y"] = 1
    # TypeError: Invalid key.
    
    d = MyDict(x=1.1)
    # TypeError: Invalid value type.
    
    d = MyDict(x=1, y=1)
    # TypeError: Invalid key.
    
    d = MyDict()
    # TypeError: Missing required key.
    

    or similar.

    EDITED to include a runtime type checking dynamic class that is easy to subclass.