Extending a `TypedDict` with non-required fields

Hello. There is something in the assignment rules involving TypedDicts inheritance that I don’t understand, or that mypy gets wrong. Assume the following TypedDict declarations:

class A(typing.TypedDict, total=False):
    a: int

class B(typing.TypedDict, total=False):
    a: int
    b: int

As expected, this is valid code:

a: A = {'a': 1}
b: B = {'a': 1}

However, unexpectedly for me, this is not:

a: A = {'a': 1}
b: B = a

At the same time:

b: B = {'a': 1, 'b': 2}
a: A = b

is considered valid, while

a: A = {'a': 1, 'b': 2}

is considered invalid, as it should.

I started looking into this because I wanted to define a TypedDict extending another with some not required fields but mypy does not like me to assign instances of the base dict to the extended one.

This is with mypy 1.13.

Am I missing something?

Mypy is correct here.

While B is a subtype of A, the converse is not true. That means B is assignable to A, but A is not assignable to B.

Keep in mind that TypedDicts are structural types. A value that conforms to A has a key named a, but it can also have any other keys with other names (including b), and the values for these keys can be any type. That means it’s not type safe to assign a value of type A to B because B indicates that the value associated with b must be an int.

For more details about TypeDict assignability rules, refer to the Python typing spec.

1 Like

If you take the time to read the documentation, you’ll find a feature called NotRequired that seems exactly for this purpose:

@JamesParrott The author used total=False, which is intended to behave identically to NotRequired. Please be more welcoming in your messages.

2 Likes

Thanks Eric. However, I’m confused by this

as it is not what I observe. The code below is considered invalid:

class A(typing.TypedDict, total=False):
    a: int

a: A = {'a': 1, 'b': 2}

as is this:

a: A = {'a': 1}
a['b'] = 2 

It seems that it is enforced that a value conforming to A cannot have any key other than a.

What am I missing?

Noted.

Sorry Daniele

2 Likes

That code is considered invalid because of a special provision for dictionary literal expressions assigned to a TypedDict. Type checkers generate an error if a dictionary literal contains a key that is not found within the target TypedDict even though such a dictionary is technically type compatible with that TypedDict. This provision was introduced by mypy to help catch misspellings and typos, and all other major type checkers (including pyright, pytype and pyre) also implement this provision. See this mypy issue for historical context. [Note: I thought this behavior was dictated by the typing spec, but I can’t find it there. We should probably fix this omission.]

Here’s a modified code sample that demonstrates my point from above:

Code sample in pyright playground

from typing import NotRequired, TypedDict

class B(TypedDict):
    a: int
    b: NotRequired[int]

class A(TypedDict):
    a: int

b = B(a=1, b=1)
a: A = b
1 Like

Thanks again, Eric. I found this behavior very confusing and contradicting the structural type principle. Mentioning it in the documentation would be a very good thing.

This leaves me wondering if what I would like to do is possible: is a way to type a dictionary B that should include only the keys found in a TypedDict A with the corresponding values having consistent types, plus a few more other optional keys, in a way that an instance of A can be assigned to a name annotated to have type B?