If your goal is to add an immutable TypedMapping
counterpart to a TypedDict
, then I don’t think it should support hybrid mutable/immutable objects. This will be conceptually confusing for users, won’t compose well with existing and new typing features, and create a bunch of challenging edge cases for type checkers. As just one example, it’s not clear whether the update
method should be present on an object that is a hybrid of a TypedDict
and TypedMapping
. If it is, what argument type should it accept?
We should also learn a lesson from the original TypedDict
design, which introduced the notion of “required” and “not required” fields but allowed them to be combined only through a cumbersome inheritance mechanism where some layers of the class hierarchy were marked total
and others were not. This problem was addressed by PEP 655, which introduced the more flexible Required
and NotRequired
special forms. Even if we were all convinced that hybrid mutable/immutable objects weren’t a problem, this design would create a new problem similar to the one that needed to be fixed by PEP 655.
If your goal is to add support for read-only keys to TypedDict
(and I think that’s a reasonable goal), then I’d like to propose an alternative approach. (It sounds like this was the direction you were exploring with your earlier iterations of the idea.) Rather than add a new TypedMapping
type, we could introduce a new special form called ReadOnly
. It would be similar to Required
and NotRequired
and would be applied to type declarations for keys within a TypedDict
. This ReadOnly
form would need to compose with Required
and NotRequired
, but I think that’s easily doable.
class MyHybridDict(TypedDict):
a: ReadOnly[str]
b: ReadOnly[Required[int]]
c: str
A new read_only
keyword parameter could also be added to TypedDict
(similar to total
) that marks all fields as read-only even if they are not annotated as ReadOnly
.
@final
class MyMapping(TypedDict, total=False, read_only=True):
a: str
b: int
If all of the keys in a TypedDict are marked read-only and the class is marked @final
, then the type would be considered compatible with Mapping[str, object]
. If any keys are not read-only or the class is not marked @final
, then it would not be compatible with Mapping[str, object]
.
Here are some benefits I see to this alternative proposal:
- If the goal is to introduce the ability to introduce read-only keys for
TypedDict
, then it more directly models this intent. That makes it conceptually simpler for users to understand.
- It provides a standardized way to “spell” the type of a field that is read-only. This is important for error messages and language server features (hover text, etc.).
- The rules for inheritance are conceptually simpler. We simply need to define what happens when a subclass overrides a key within a base class and the read-only attribute disagrees. (It’s fine to override a writable key as read-only but not the other way around.)
- It composes better with other features that have been discussed in this part of the type system including inlined TypedDict definitions (e.g.
dict[{"a": int, "b": ReadOnly[str]}]
) and the ability to specify that a TypedDict supports an arbitrary number of additional keys that match a specific type. It’s less clear to me how these new features would work with the currently-proposed design.
- I could also see the
ReadOnly
special form being useful in other typing contexts in the future — for example, for protocol attributes and dataclass fields.
In summary, I think this PEP should choose one of two different routes:
- Introduce a
TypedMapping
type that is an immutable counterpart of TypedDict
. Most Python users have a good understanding of the relationship between Mapping
and dict
, and it’s reasonable to assume that the same relationship exists between TypedMapping
and TypedDict
.
- Introduce something like a
ReadOnly
special form and allow it to be applied to keys within a TypedDict
.