Infer dictionary literals as TypedDict

Hi all, I would like to get the conversation going on introducing a mechanism to have dictionary literals be inferred as TypedDicts. The issue has been discussed here and there a few times (see the links below) but to my knowledge no attempt has been made yet to flesh out the details, let alone reach consensus. I would like to change that. :slight_smile:

The issue:

The type of

foo = {
    "a": 1,
    "b": 2,
}

is currently being inferred as dict[str, int] by e.g. Mypy and Pyright, but there are many cases in which foo’s keys will not change and one would therefore like to have its type to be inferred more narrowly as

TypedDict[{
    "a": Literal[1],
    "b": Literal[2],
}]

without having to write out the full type as type annotation.

Here, for brevity I am using the proposed (PEP 764) syntax for inline TypedDicts to describe a closed typed dictionary (in the sense of PEP 728), but the question of how to infer foo as TypedDict shouldn’t depend on whether or not PEP 764 gets accepted or not.

The use case for TypedDict inference comes up frequently when using dictionary literals to build & return structured data with a fixed set of keys “on the fly”. In such cases, defining separate dataclass/TypedDict types first is often particularly undesirable because there will only ever be a single instance of the data structure in the first place (namely the dictionary literal) and so the type declaration would always follow the dictionary literal’s structure, anyway, and have to be kept in sync.

Prior discussions:

Prior art:

  • Enabling narrow type inference for a given dictionary is somewhat similar to TypeScript’s as const syntax, whose behavior I outlined here.

Existing ideas:

So far I have come across the following ideas:

In my opinion, introducing such a type annotation alone comes with one significant downside, though, particularly compared to TypeScript’s as const: It would be impossible to guide type inference for dictionary literals within more complex, nested expressions, without spelling out the entire expression’s type. Consider e.g.:

foo = (
    "hello",
    "world",
    {
        "a": 1,
        "b": 2,
    }
)

Here, foo is currently inferred as Tuple[Literal["hello"], Literal["world"], dict[str, int]] but we would want to infer Tuple[Literal["hello"], Literal["world"], TypedDict[{ … }]] without having to annotate foo with Tuple[Literal["hello"], Literal["world"], TypedDict] or something similar. I have elaborated on this topic here.

For this reason, my thoughts are going in the direction of introducing syntax (or at least a helper function in typing) to guide type inference at the expression level, i.e. to annotate expressions, e.g.:

foo = (
    "hello",
    "world",
    {
        "a": 1,
        "b": 2,
    } as TypedDict
)

The use case might be broader than inferring TypedDict alone: For instance, I might have a list or tuple literal which I’d like to infer as Sequence or a dictionary literal which I’d like to infer as Mapping. In fact, the use case might also be broader than literal expressions: I might have an expression of type A which I want the type checker to treat as type B while still ensuring that the expression is compatible with type B. This would be reminiscent of TypeScript’s satisfies, while also doing a type cast.

Maybe these are two separate topics, though? Maybe not? Either way, I’d be grateful for feedback and very interested in hearing your thoughts!

1 Like

Somewhat related: Pyrefly is able to infer a heterogeneous dict type (an “anonymous typed dict”) from a dictionary literal, where different keys can have different types. But for your specific example it would not work, because all the literal types are promoted so we’d still end up with `dict[str, int]`

Just to throw an idea, maybe the Final variables could be inferred as TypedDict in the same way that x: Final = 123 is inferred as Literal[123]? Right now, the Final spec doesn’t seem to rule out use of it in function bodies, and all the type checkers I could think of (mypy, pyright, ty, pyre, pyrefly) handle Final in them just fine. The limitation of this would be that you can’t reassign such a variable with a different compatible type, but it would mean being able to use the existing constructs.

Personally, I guess I expect Final type hint to resolve to the most specific type, so such an idea would definitely follow that. I might be alone in this expectation, though, which would mean people would be getting diagnostics they really don’t want to get when using Final without an explicit type.

2 Likes

I guess it depends on why you want that inference. If you want that inference so that it automatically agrees with an API that expects a TypedDict, then I think the most elegant option here is : TypedDict as an annotation next to a literal meaning to infer from that literal.

But I’m not sure how a specified inference is useful to you over tool specific behavior unless we’re looking at ensuring public API agreement. In that case, the other end accepts a specific TypedDict, so that would have to be defined anyhow right?

The other option you provided, TypeScript’s as const doesn’t fit well into python in my opinion. It would be significantly better for type checkers to stop eagerly inferring a static type all together, instead inferring a gradual type based on the known information, and just check that all use is consistent with the expressed type information, and that the program never accumulates conflicting constraints upon a gradual type within the same code path.

1 Like