TypedDict doesn't expose its keys as a distinct type, nor is there really a good way to define such a type.
Not recommended
Some people use typing.Literal, but that is intended for, well, literals. Use key: Literal["first_name", "last_name", "age"] allows c.set_info("first_name", "bob"), but not k = "first_name"; c.set_info(k, "bob"), because k is not a literal. I don't recommend doing this, but I felt it needed to be addressed.
Better
Another alternative would be to explicitly define a separate enumerate type and use its members, rather than raw keys, as arguments.
class CustomerKey(enum.StrEnum):
FIRST_NAME = "first_name"
LAST_NAME = "last_name"
AGE = "age"
# This could (somewhat awkwardly) also be defined as
#
# CustomerKey = enum.StrEnum(dict(zip(map(str.upper, UserType.__annotation__),
# UserType.__annotation__)))
#
# to avoid repetition of the exact keys.
class Customer:
...
def set_info(key: CustomerKey, val: Any):
self._userInfo[key] = val
Better still
This is assuming you don't have a good reason to store the attributes separately, and only use the TypedDict as the argument to a class method, for example,
@dataclasses.dataclass
class Customer:
first_name: str
last_name: str
age: int
@classmethod
def from_dict(cls, d: UserType):
return cls(**d)
Best (hypothetical)
Don't read too much into what follows. I'm mostly thinking out loud about ways that TypeDict could expose the information it contains as type hints.
In an ideal world, the keys of a TypedDict would themselves form a type. Then perhaps you could write something like
class Customer:
...
def set_info(key: UserType.keys, val: Any):
...
Even better, there would also be a way to tied the value assigned to key to a type that could be used to annotate val, so that if key == "first_name", then val must have type `str. I imagine this would require some mix of dependent types (where types can depend on value), singleton types (where for every value there is an associated type that contains only that value) and generics. Something vaguely like
def set_info(key: UserType.keys, val: UserType[key])
Answer from chepner on Stack OverflowPython - TypedDict type hint keys - Stack Overflow
Interesting use cases for a TypedDict
How do I type hint a function using TypedDict that mutates a dictionary in Python? - Stack Overflow
TypedDict type is not giving any error despite using extra keys and using different datatype for a d
Videos
TypedDict doesn't expose its keys as a distinct type, nor is there really a good way to define such a type.
Not recommended
Some people use typing.Literal, but that is intended for, well, literals. Use key: Literal["first_name", "last_name", "age"] allows c.set_info("first_name", "bob"), but not k = "first_name"; c.set_info(k, "bob"), because k is not a literal. I don't recommend doing this, but I felt it needed to be addressed.
Better
Another alternative would be to explicitly define a separate enumerate type and use its members, rather than raw keys, as arguments.
class CustomerKey(enum.StrEnum):
FIRST_NAME = "first_name"
LAST_NAME = "last_name"
AGE = "age"
# This could (somewhat awkwardly) also be defined as
#
# CustomerKey = enum.StrEnum(dict(zip(map(str.upper, UserType.__annotation__),
# UserType.__annotation__)))
#
# to avoid repetition of the exact keys.
class Customer:
...
def set_info(key: CustomerKey, val: Any):
self._userInfo[key] = val
Better still
This is assuming you don't have a good reason to store the attributes separately, and only use the TypedDict as the argument to a class method, for example,
@dataclasses.dataclass
class Customer:
first_name: str
last_name: str
age: int
@classmethod
def from_dict(cls, d: UserType):
return cls(**d)
Best (hypothetical)
Don't read too much into what follows. I'm mostly thinking out loud about ways that TypeDict could expose the information it contains as type hints.
In an ideal world, the keys of a TypedDict would themselves form a type. Then perhaps you could write something like
class Customer:
...
def set_info(key: UserType.keys, val: Any):
...
Even better, there would also be a way to tied the value assigned to key to a type that could be used to annotate val, so that if key == "first_name", then val must have type `str. I imagine this would require some mix of dependent types (where types can depend on value), singleton types (where for every value there is an associated type that contains only that value) and generics. Something vaguely like
def set_info(key: UserType.keys, val: UserType[key])
For certain use cases a certain amount of redundancy is still required to work with the type-checking systems in python... In particular, dependent types and the ability to specify types aren't handled as well as typescript, say, but that's because the types sort of "exist" in a way that forces the python typing crowd to make certain assumptions about the way types are formed.
Anyways, very close to what you might be looking for is something like:
from typing import TypedDict, Unpack
class PartialUserType(TypedDict, total=False):
age: int
first_name: str
last_name: str
class Customer:
def __init__(self):
self._userInfo: PartialUserType= {
"first_name": "John",
"last_name": "John",
"age": 27,
}
def set_info(self, **kwargs: Unpack[PartialUserType]):
self._userInfo |= kwargs
Which results in editor help like so:

The main drawback to this approach is that the PartialUserType will now also consider the following valid instances:
x: PartialUserType= {}
y: PartialUserType= {'age': 0}
z: PartialUserType= {'first_name': '', 'last_name': ''}
Unfortunately inheritance does not allow us to flip that flag in a useful way, so the only way to get the behavior of both the UserTypes is to explicitly write them both. One idea is to use code generation, the basic idea shown below:
from typing import TypedDict, Unpack
class UserType(TypedDict):
age: int
first_name: str
last_name: str
if __name__ == '__main__':
with open('./partial_types_generated.py', 'w') as f:
print("# THE CONTENT IN THIS FILE HAS BEEN GENERATED, NOT NOT MODIFY BY HAND!", file=f)
# do whatever imports you may need
print("from typing import TypedDict", file=f)
print("\n# partial types\n", file=f)
for typed_dict in [
# Gather up all the dicts you want partials for
UserType,
]:
name = f"Partial{typed_dict.__name__}"
annotations = {k: v.__name__ for k, v in typed_dict.__annotations__.items()}
print(f"{name} = TypedDict('{name}', {annotations}, total=False)", file=f)
This script gives the file:
# THE CONTENT IN THIS FILE HAS BEEN GENERATED, NOT NOT MODIFY BY HAND!
from typing import TypedDict
# partial types
PartialUserType = TypedDict('PartialUserType', {'age': 'int', 'first_name': 'str', 'last_name': 'str'}, total=False)
Python has a couple of components, more or less structured, that kind of try to achieve similar goal (imo): TypedDict, NamedTuple and Dataclass. I struggle a bit to see what advantage you would have in using a TypedDict over the others since you expect to know the keys of your dictionary in advance. The key access or the flexibility maybe? I am sure that since it exists and is used it must have good use cases which I’d like to understand, if anyone has one to share!