PEP 705 – TypedMapping

Hi all,

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.

See also the prior discussion on typing-sig.

4 Likes

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.

2 Likes

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?

the phrasing “mutator methods” confused me

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.

Another possibility you could maybe discuss in “Rejected Alternatives” is something like:

from typing import ReadOnly, TypedDict

class Movie(TypedDict):
    name: ReadOnly[str]
    year: ReadOnly[int | None]

where any field marked as ReadOnly[] may not be mutated.

I think this would give you the same features? The main downside I see is the verbose syntax; especially if you combine it with NotRequired:

from typing import NotRequired, ReadOnly, TypedDict

class Movie(TypedDict):
    name: ReadOnly[str]
    year: ReadOnly[NotRequired[int | None]]