Is there a way to get TypedDict-level type safety on a dict subclass?

I’m wondering if it’s possible to get TypedDict-like type safety on a subclass of dict.

For example, I’d like something like this:

class Point(RuntimeTypedDict):
    x: int
    y: int

    @classmethod
    def from_tuple(cls, p: tuple[int, int]) -> Self:
        x, y = p
        return cls(x=x, y=y)

p = Point.from_tuple((0, 0))
p["x"] = None  # ❌ Type error expected
y = p["y"]      # ✅ inferred as int
assert isinstance(p, Point)

I know I could achieve similar behavior with a dataclass or a more structured object, but in my case that’s not practical because:

  • I’m contributing to third-party code,
  • The code extensively uses dicts with known keys and depends on them being actual dict objects,
  • Refactoring to use dataclasses or another structure would be impractical.

So, what I’m really looking for is type safety (like TypedDict) while keeping the object as a real dict subclass.

2 Likes

FWIW I tried using the functional-call syntax. It felt like a hack, and still didn’t work:

PointT = TypedDict('PointT', Point.__annotations__)
        
# print(Point.__annotations__)
p: PointT = Point.from_tuple((0, 0))
typed_dict_test.py:17: error: TypedDict() expects a dictionary literal as the second argument  [misc]

This idea was brought up by me a while ago with the ReadOnly pep before it moved away from TypedMapping which is generally applicable to dataframes and other things where keys are known without enforcing the type is a dict subclass

1 Like

TypedDict is specified to only represent instances of dict itself, not subclasses of dict. This is not currently possible.

2 Likes

In the following example, pyright complains on the definition of B (with All base classes for TypedDict classes must also be TypedDict classes and another error) but it still infers all the types in g() correctly:

from typing import TypedDict

class TD(TypedDict):
    x: int
    y: int

class A(dict):
    def f(self) -> str:
        return ""

class B(A, TD): pass

def g(x: B) -> None:
    reveal_type(x["x"])  # int
    reveal_type(x.f())  # str

So, you could just silence the errors on B and do it like this.

1 Like

The fact that it infers types in g “correctly” (is it correct to ignore the specification and guess that people will be doing something like this as a hacky workaround rather than improve the type system?) currently isn’t something I’d sugest anyone rely on. Any definition which only works when ignored is fragile to typechecker changes. It would be better if typecheckers refused to typecheck such code to prevent incorrect reliances like this. (specifically some sort of message like Note: type of x[“x”] is not reliably known due to an ignored definition)

This is where you put a use of Any, document it for api surface, and add a runtime check if a check is needed, until working to improve the type system to be more expressive. Kindof the whole point of gradual typing, type what you can, gradually improve that.