← Back to context

Comment by oivey

1 year ago

TypedDicts are a really disappointing feature. Typing fails if you pass it a dictionary with extra keys, so you can’t use it for many structural typing use cases.

It’s even more disappointing because this isn’t just an oversight. The authors have deliberately made having additional keys an error. Apparently, this even a divergence from how TypeScript checks dictionaries.

For what it's worth, TypedDict was a bit ahead of it's own time. Python 3.12 is really the turning point for being able to leverage them effectively for stuff like **kwargs[1][2]

    from typing import TypedDict, Unpack, NotRequired

    class Movie(TypedDict):
        name: str
        year: NotRequired[int]

    def foo(**kwargs: Unpack[Movie]) -> None: ...

[1] https://typing.readthedocs.io/en/latest/spec/callables.html#... [2] https://peps.python.org/pep-0692/

  • How does that work for partial forwarding? Back when I last checked there was no imagining a DRY solution for that even for linear cases, let alone diamond inheritance.

      # Simple case, any halfway-decent type system should be able to handle this.
      def inner(*, i): pass
      def middle(*, m, **kwargs): inner(**kwargs)
      def outer(*, o, **kwargs): middle(**kwargs)
      kwargs = ...; outer(**kwargs)
    
      # More complicated case, but fairly common in Python code.
      class A:
        def __init__(self, *, a):
          pass
      class B(A):
        def __init__(self, *, b, **kwargs):
          super().__init__(**kwargs)
      class C(A):
        def __init__(self, *, c, **kwargs):
          super().__init__(**kwargs)
      class D(B, C):
        def __init__(self, *, d, **kwargs):
          super().__init__(**kwargs)
      kwargs = ...; D(**kwargs)
    
      # An even more complicated case involves `kwargs.pop()`, forwarding without `**`.
    

    Can the above be typechecked yet?

To be fair, that behavior on typescript's part is a major hole for bugs to slip through.

Specifically: absent optional keys + extra keys is fundamentally indistinguishable from miseptl keys.

  • `interface` in TS allows extra keys, `type` does do not. Usually best to use `type` and add an intersection with some other type if you want extras (`Record<string,unknown>` is all right for arbitrary extra keys)

  • The current situation is worse, IMO. Lots of code that could be checked cannot, and lots of code that could have a more clearly defined interface does not.

I'm not sure I understand.

TypedDicts are disappointing because you can't partially define a type? That seems like a success

In go you would need to define all fields in your struct and if you needed unstructured data you would have to define a map, which even then is partially typed

How should extra keys behave in "typed python?"

  • Go is a terrible point of comparison. Python’s type system should not pointlessly match what other languages have chosen due to some myopic dogma.

    Python is a dynamic language where one of its major features is structural subtyping, aka duck typing. It’s effectively an alternative to inheritance. Features have been added to help support this, like TypedDicts and Protocols already. They don’t go far enough.

    I want to be able to say “this function argument is a dictionary that has at least the keys a, b, and c.” It gives the contract that the function only accesses those keys, and others will be ignored. The type checker can check that the function doesn’t access undeclared keys and the annotation helps communicate to client code what the interface is supposed to be.

    Lots of Python’s bolted on type checking seems to be straight jacketed to match what’s in C, Java, Go, etc. Those languages don’t contain the only possible type systems or static checkers. There’s a serious lack of imagination. Python’s type system should be designed around enabling and making safer what the language is already good at.

    • > I want to be able to say “this function argument is a dictionary that has at least the keys a, b, and c.” It gives the contract that the function only accesses those keys, and others will be ignored.

      Do we agree that this is the behavior of regular dicts in python? How should TypedDicts be different?

      Surely, the goal of typing in python should not be to match behavior without