I’d like to put PEP 705 forward for discussion. It is proposing a read-only mapping protocol, similar to TypedDict but without the mutator methods. This permits correctly type hinting read-only parameters, which currently are very hard to make fully general.
I haven’t used Final much so excuse my ignorance, but beyond the fact that this implements the Mapping protocol, how is this proposal different from annotating a TypedDict with Final or @final?
Marking a TypedDict class final prevents it being subclassed. That is currently interpreted by typecheckers as meaning all keys not specified in the type definition must be absent. It has no effect on whether instances are mutable or not.
The @final decorator means that a class cannot be subclassed. The Final modifier means that a variable cannot be reassigned. Both of those are orthogonal to the problems that TypedMapping solves, which are about mutability of the object itself.
Thanks for this, I think it will be helpful! A couple minor questions:
In section “Multiple inheritance and TypedDict”, the phrasing “mutator methods” confused me a bit since I think of that as referring to just setitem etc. independent of a particular key. If a mutating operation is done with something that is not statically known to be a key in the TypedDict, I assume that should cause a checker error? e.g. assert somestr in movie; movie[somestr] = None
Re:
but is not itself a protocol unless it inherits directly from TypedMapping or Protocol
Isn’t a class currently not a protocol unless it inherits directly from Protocol? Why would we add TypedMapping to that rule?
That’s a good point. I think of them as separate methods (which they would be in e.g. Java) but in Python they’re overloads on a single method. Would it be clearer if it was phrased this way?
adds mutator method overloads only for fields it explicitly (re)declares
The code sample would change to
movie["year"] = 1985 # Fine; setitem mutator overload for "year" added in definition
movie["name"] = "Terminator" # Type check error; "name" not in setitem overloads
Isn’t a class currently not a protocol unless it inherits directly from Protocol?
Currently, yes. With the addition of TypedMapping, however, it becomes necessary to distinguish between a “protocol” lower-case-p and Protocol upper-case-P. A TypedMapping type is a protocol type, lower-case-p, because it acts, once created, exactly like any other protocol†. Protocol is now just one of two ways to create a protocol type. A simple motivating example:
class One(Protocol):
def clear() -> None: ...
class Two(TypedMapping, total=False):
key: int
class Three(One, Two, Protocol):
pass
class Four(One, Two, TypedMapping):
pass
Classes Three and Four are structurally identical: they can be used completely interchangeably. It makes sense to me therefore to just call them both protocols, lower-case-p.
† Except that TypedDict will allow mixing in a TypedMapping protocol, but will not currently allow mixing in a Protocol protocol even if it logically could. This is a matter of pragmatism rather than necessity, though.