PEP 764: Inlined typed dictionaries

Indeed, I think I made my argument about readability a bit unclear: what I wanted to say is that introducing call expressions in type expressions would make things confusing for users. Yes, using curly braces will require introducing the singularity in type_expression production, but we’re not introducing a “new kind of type form creation” (currently such “creation” happens with [], and curly braces for inline TDs are specified inside [], same as parentheses in tuple_type_expression for that matter). The confusion may come when writing such an annotation:

dict[Literal['a'], list[TypedDict({'b': int})...
#   |       |          |         |- `(` ugh, why not `[`?
#   |       |          |
#   |       |          |- 3rd `[` used, all good..
#   |       |
#   |       |- 2nd `[` used..
#   |        
#   |- 1st `[` used..

But I get the advantage of using the existing functional syntax to be able to provide meta arguments. And given that the functional syntax already exists (when assigning to a variable), the confusion might not be that important. What needs to be sorted is whether – for static type checkers – evaluating call expressions is really an issue.

I’ll add that this keyword syntax used to exist, was deprecated and removed in 3.13. Adding it back will only make things more confusing.

I believe this would result in an attractive nuisance for users unaware of the limitations of this syntax, especially as it wouldn’t be possible to introspect unions of two dictionaries (it wouldn’t fail, but would wrongly assume a single dictionary was hinted; this page goes into more detail).

2 Likes

I would also find this capability useful, although I have a slightly different syntax in mind:


In the PEP I see the syntax:

from typing import TypedDict

def get_movie() -> TypedDict[{'name': str, 'year': int}]:
    return {
        'name': 'Blade Runner',
        'year': 1982,
    }

I’d like to additionally propose that TypedDict[…] (with a literal …) be recognized. It would infer the fields and their types using type checker inference rules. For example:

from typing import TypedDict

def get_movie() -> TypedDict[…]:
    return {
        'name': 'Blade Runner',
        'year': 1982,
    }

This syntax would allow big dictionaries to be defined/returned, yet be given the obvious TypedDict type without needing to spell it out explicitly.

This automatic/inferred TypedDict[…] syntax would eliminate a lot of boilerplate in a particular large .py file from one of my (closed source) projects. (Trivia: It’s the same file that inspired me to propose TypedDict itself many years ago.)

9 Likes

Pretty much any time I make a dict with literal string keys, I’m wishing the type-checker would infer it as a TypedDict
So I think it should infer the TypedDict return type like this:

def get_movie():
    return {
        'name': 'Blade Runner',
        'year': 1982,
    }
3 Likes

I think this would be pretty great, and would replace a ton of my tuple usage while removing type repetition and improving ergonomics (because fields now have names).

The DBA in me doesn’t like the de-normalized design (having to repeat the name). Instead of:

def get_movie() -> TypedDict[{'name': str, 'year': int}]:
    return {
        'name': 'Blade Runner',
        'year': 1982,
    }

why not

def get_movie() -> TypedDict:
    return {
        'name': str = 'Blade Runner',
        'year': int = 1982,
    }
1 Like

Because the interior of functions shouldn’t affect the publicly declared type. (for multiple reasons, from using types to enforce stable API boundaries, to the fact that the “bring your own type checker” situation depends on not requiring inferring from body)

Of course, if this is a problem, just don’t use the inlined form:

class MovieSpec(TypedDict):
    name: str
    year: int


def get_movie() -> MovieSpec:
    return MovieSpec(name='Blade Runner', year=1982)
1 Like

So what’s

def get_movie() -> TypedDict[{'first': str, 'last': str]:
    return {
       'given': 'Alan',
       'surname':'Turing'
    }

return?

a dict that doesn’t match the annotation given at runtime, and a type checking error when typechecking the project containing that function at typechecking time, assuming that the inlined typed dicts are still closed by default.

2 Likes

I think repeating information is only really a problem if you don’t have something that automatically tells you when the 2 pieces of information get out of sync before you deploy.

def get_movie() -> TypedDict[{'first': str, 'last': str]:
    return {
       'given': 'Alan',
       'surname':'Turing'
    }

In this case, you can have something in CI that automatically tells you when the keys don’t match.

Besides the other reasons given, this breaks existing syntax.

What’s it break? It’s not currently valid, is it? Python3.14 gives me:

File “/tmp/td.py”, line 3
‘name’: str = ‘Blade Runner’,
^
SyntaxError: invalid syntax

That’s what I meant–you’ve created a different syntax for this sort of dictionary, which isn’t valid for other dictionaries.

1 Like

Yes indeed. That’s why it’s a enhancement :wink:

I believe the machine should work for the people instead of making the people work for the machine. A syntax that:

  • requires a human to repeat a name (creating an opportunity for a typo)
  • works without complaint at runtime, but
  • but then complains when type checked

Is machine (typing) centric, not human centric.

2 Likes

@davidfstr

I’d like to additionally propose that TypedDict[…] (with a literal …) be recognized. It would infer the fields and their types using type checker inference rules. For example:

I like the idea but how would you handle the nested case? Let’s say you’re building a complicated data structure using lists and dictionaries and now you would like to change what type the type checker infers for a given substructure. E.g.

complicated_data_structure = {
  # …

  # The following dict might be buried several levels deep in additional dicts/lists/tuples:
  relevant_movie: {
      'name': 'Blade Runner',
      'year': 1982,
  } # How can I tell the type checker to infer the narrowest type possible for this particular expression? (Similarly to TypeScript's `as const`?)

  # …
}

In light of this, I’m increasingly convinced we need a way to control the inferred type of expressions (like the infer_const(…) helper from my previous comment), not just of variables or return statements.

@Viicos are you still interested in this PEP? It’s been discussed for a long time, perhaps it’s time to submit it for approval.

Sorry for letting this one stale, I am (maybe overly) cautious about what I think would be the right way forward, and wasn’t too satisfied with this approach. That’s why I played with an alternative idea (Inlined typed dicts and typed dict comprehensions - #12 by Viicos / PEP 9999 – Inline type expressions and inline typed dictionaries | peps.python.org), and together with Idea: Simpler and More Expressive Type Annotations / PEP 827: Type Manipulation it might make this PEP obsolete.

But I do acknowledge the demand for this one, and maybe it isn’t that much of an issue to have this syntax and potentially a more idiomatic one in the future?

Not being familar with as const, I’m not sure what you’re trying to do in the example provided.

As written, your example I would expect to infer to be a nested anonymous (mutable) TypedDict.

@davidfstr

Not being familar with as const, I’m not sure what you’re trying to do in the example provided.

TypeScript’s as const infers the narrowest-possible type in some sense and is added to expressions, not type annotations.

Let’s back up for a moment and consider the following Python code:

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

Right now, Mypy and Pyright infer foo’s type to be dict[str, int]. However, in many cases (though not all!) I’d want foo to be typed more narrowly as

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

with the TypedDict being closed as mentioned in PEP 764 – Inline typed dictionaries | peps.python.org . I think up until here we agree.

Now, in PEP 764: Inlined typed dictionaries - #82 by davidfstr you proposed using a TypedDict[...] (... being the literal ellipsis) as type annotation to have the type checker do this more narrow type inference. E.g. if I understand your proposal correctly

foo: TypedDict[...] = {
    "a": 1,
    "b": 2,
}

would produce the TypedDict[{ "a": …, "b": … }] type mentioned above.

Now, what I was getting at was that sometimes we use dictionaries within larger expressions, the larger expression being e.g. another dictionary, a list, a tuple, some function call, … How would we trigger narrow type inference for the dictionary “subexpression”? For a nested dictionary you said in your most recent comment that

bar: TypedDict[...] = {
    "foo": {
        "a": 1,
        "b": 2,
    }
}

should give a nested TypedDict type, i.e. TypedDict[{ “foo”: TypedDict[{ "a": …, "b": … }] }].

However, I don’t think this is flexible enough for two reasons:

  • I might not want to apply narrow type inference to the entire dictionary but only to the foo part, with the top-level type staying a dict[str, something].
  • It doesn’t generalize well to other composite expressions, in which the top level structure is not a dictionary.

To illustrate the second point, consider the following two examples:

# Example 1
foo = (
    "hello",
    {
        "a": 1,
        "b": 2,
    }
)  # currently inferred as Tuple[Literal["hello"], dict[str, int]] but we would want Tuple[Literal["hello"], TypedDict[{ … }]]
# Example 2
def bar[T](val: T) => list[T]:
    return [val]

def func():
    return bar({
        "a": 1,
        "b": 2,
    })  # inner dictionary is currently inferred as dict[str, int], so func()'s return type is list[dict[str, int]]. However, we would want list[TypedDict[{ … }]].

In both cases, there are of course workarounds, like

  • extracting the dictionary literal into a separate variable, type-annotated with TypedDict[…], or
  • adding a type annotation for the entire composite expression, like declaring foo: tuple[Literal[“hello”], TypedDict[...]] = … in the first example.

However, I find these workarounds suboptimal and would like to enable narrow type inference not through a type annotation like your proposed TypedDict[...] but through some hint I can add to the expression itself.

Here I am getting back to TypeScript, whose as const syntax can be appended to any expression:

const bar = {
  baz: "hello, world!",
  foo: {
    a: 1,
    b: 2
  },
};  // inferred as { baz: string; foo: { a: number; b: number; } }

const bar = {
  baz: "hello, world!",
  foo: {
    a: 1,
    b: 2
  } as const,  // <--- "as const" applies to the preceding expression 
};  // inferred as { baz: string; foo: { readonly a: 1; readonly b: 1; } } . Notice how the type of baz has stayed the same and did not turn into a literal type "hello, world!".

In Python, I would want something similar (and similarly convenient). Anyway, we should probably take the discussion elsewhere as it’s a bit off-topic. EDIT: I have started a separate thread here.

1 Like

@Viicos While I am much in favor of simpler and more expressive type annotations, as well as type manipulation, I do think there is still a lot to be sorted out there. Same goes for TypedDict comprehensions and general type manipulation, which – in addition – are mostly orthogonal topics. Meanwhile, your PEP has seen a lot of positive feedback, and seems pretty much ready for submission(?) So I wouldn’t want to let perfect be the enemy of good here. On that note:

But I do acknowledge the demand for this one, and maybe it isn’t that much of an issue to have this syntax and potentially a more idiomatic one in the future?

Yes, I think so. With more expressive type annotations, inline TypedDicts would hopefully look nicer, too, but that is true for a lot of type annotations, not just TypedDicts. So I would first focus on getting a workable solution that provides real practical benefit (which I think PEP 764 does). Later on, another PEP might introduce cleaner syntax for type annotations in general. But that also requires figuring out what type constructs are popular and would benefit from simpler syntax in the first place. Right now, I’d argue TypedDict’s popularity is being harmed by the fact that it’s almost as cumbersome to use as dataclasses (in the typing context). PEP 764 would improve the situation significantly.