rasen58
rasen58

Reputation: 5081

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?

Upvotes: 2

Views: 1907

Answers (3)

norok2
norok2

Reputation: 26886

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.

Upvotes: 2

Brian61354270
Brian61354270

Reputation: 14403

Both dataclasses and named tuples provide key checking on construction. Using your examples:

from dataclasses import dataclass
from typing import NamedTuple

@dataclass
class X1:
    id: int

class X2(NamedTuple):
    id: int

X1(id=4) # ok
X2(id=4) # ok

X1(id=4, thing=3)
# TypeError: __init__() got an unexpected keyword argument 'id'

X2(id=4, thing=3)
# TypeError: __init__() got an unexpected keyword argument 'id'

Do note that dataclasses do not prevent you from assigning to "bad" attributes after construction. The following code is still valid:

x1 = X1(id=4)
x1.thing = 3  # still ok

In contrast, named tuples are immutable, so assigning to arbitrary attributes at runtime isn't possible:

x2 = X2(id=4)
x2.thing = 3
# AttributeError: 'X2' object has no attribute 'thing'

Upvotes: 1

Dmitry Messerman
Dmitry Messerman

Reputation: 366

It is possible to use mypy package.

sudo pip3.9 install mypy
rehash
cat typedDict_ex.py
#!/usr/bin/python3.9
from typing import TypedDict
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!
mypy typedDict_ex.py
typedDict_ex.py:10: error: Extra key "thing" for TypedDict "X"
Found 1 error in 1 file (checked 1 source file)

Upvotes: -2

Related Questions