PEP 705 – TypedMapping

If you’re interested in playing with the ReadOnly mechanism proposed above, I’ve added support for it in the latest version of pyright (1.1.310). I find that it’s easier to evaluate a new language or typing feature if I can experiment with it in a code editor.

4 Likes

I am planning on raising a new PEP for the ReadOnly special form, rather than modifying PEP 705, as it will be a significant rewrite. At that point I will withdraw the TypedMapping PEP, unless there is someone who wants it deferred?

Will this new PEP propose ReadOnly for protocol attributes in addition to TypedDict?

Can’t you use properties for that?

I believe you can use properties now, but this would be a shorter way to express it. Dataclass fields also got mentioned, but I would expect that to result in modification being impossible at runtime, i.e. it should use Final rather than ReadOnly? I would mildly prefer to leave extensions to follow-up PEPs since there’s quite a lot going on already in this one.

properties carry a distinct typecheck and runtime limitation.

If you explicitly subclass from a Protocol that declares a property, then it cannot be a plain instance attribute in the subclass.

Explicit subclassing is a documented use-case in the PEP: PEP 544 – Protocols: Structural subtyping (static duck typing) | peps.python.org

from __future__ import annotations
from abc import abstractmethod
from typing import Protocol

def accepts_foo(foo: Foo):
    foo.bar

class Foo(Protocol):
    @property
    @abstractmethod
    def bar(self) -> int:
        ...

class ImplicitImplementation:
    def __init__(self):
        self.bar = 123
class ExplicitImplementation(Foo):
    def __init__(self):
        self.bar = 123 # <-- I get a type error.  How to avoid?  `ExplicitImplementation` should have write-able `bar`

accepts_foo(ImplicitImplementation())
accepts_foo(ExplicitImplementation())

Additional Discord context in case it helps me to refer back to: Discord

1 Like

Beginners are only allowed to post 2x links, so putting the 3rd in an additional post, additional Discord context: Discord

I personally consider that a bug. A class with an instance attribute is a valid structural subtype; it should be valid to directly subclass too. I would rather fix that bug than try to work around it with ReadOnly. Indeed, I think you would be forced to fix that bug anyway, or else you’d have the reverse problem – a class with a property couldn’t subclass a Protocol with a ReadOnly attribute. (Though I haven’t checked this direction.)

Of course if someone who’d tried to do it said it would be much easier to implement ReadOnly attributes than fix the property-in-Protocol bug, I’d happily reconsider :grin:

Hm, should that example be a bug? The protocol describes a property and I don’t know that changing that be an attribute is necessarily a faithful implementation. You can create a subclass with a writeable bar by adding a getter (and storing the value in _bar or something)

class ExplicitImplementation(Foo):
    def __init__(self):
        self._bar = 123

    @property
    def bar(self) -> int:
        return self._bar

    @bar.setter
    def bar(self, value):
        self._bar = value

But I don’t think there’s a way for Foo to say “don’t implement a setter for this”, so there’s still a case for ReadOnly in some form

As I said, it’s a valid structural subtype. That should be the only consideration to whether it is a faithful implementation, I think. As far as structural typing goes, an attribute is just a mutable property.

1 Like

Maybe I’m thinking more about the trickiness of implementation…how would Python tell the difference between “I want this to be an attribute now” versus “I forgot to implement the property (including a setter)” [1]. And it must do that in a way that doesn’t allow read-only properties to be overwritten accidentally, since I don’t think __init__ gets any special privileges.


  1. which maybe would be more common? ↩︎

Understood. Perhaps the line “In this case a class could use default implementations of protocol members” in the PEP makes this impossible.

I guess this is the crux of the issue: is a property in a Protocol meant to (a) prevent overwriting in a subclass, (b) provide a default implementation, or (c) is it just meant to specify that a subclass must allow read access to an attribute/property? Currently there’s no way to do (c) unambiguously AFAIK, while (a) can be done with a Final attribute. Further, (a) is not the interpretation used by structural subtyping.

As such, I would expect b+c to be the valid interpretations of a property declaration in a Protocol even when subclassing, and if a subclass redeclares it as an attribute, that should remove the default implementation.

This might be awkward to implement, but I still consider it a bug that the current code raises an error instead. Low confidence though.

Are you thinking of the case where the subclass doesn’t explicitly declare an attribute, but still attempts to write it in the __init__ method? I hadn’t thought about this.

I can see more motivation now for marking an attribute ReadOnly, but it’s still worryingly complex. A separate PEP still seems like the right route to me.

But that said, I’m not aware of a PEP that changes only how type checkers work, without changing any core Python. Maybe it’s best to get this all hammered out in one go? Would value insight from PEP experts.

PEP 692 basically fits that description (though its implementation did end up changing the repr() of Unpack).

But I agree with you that changes to Protocol and TypedDict are best left to separate PEPs.

1 Like

Yeah basically. Although the example above is a little more complicated because of the abstract method–it’s erroring on instantiation, as it complains about the missing abstract method before __init__ even runs, which is guess is the answer to how __init__ is special.

Theoretically it could do that check after executing code, but I bet that would break a ton of stuff.

On the other hand, this (cursed?) version works:

class ExplicitImplementation(Foo):
    bar = None

    def __init__(self):
        self.bar = 123

accepts_foo(ExplicitImplementation())  # no problem

Getting back to the topic of the new TypedMapping PEP (705)…

If you’re not planning to continue PEP 705 (TypedMapping) yourself I think it would be appropriate to Withdraw it (rather than to Defer it).

FWIW, I do think functionality of a “TypedMapping” - an immutable TypedDict - would be generally useful. However I don’t currently have the bandwidth to attempt to continue the design/implementation of such a PEP myself at this time.

Thanks @erictraut ! This will be helpful for if/when someone picks up the TypedMapping PEP again.


Strongly agree. +1

There are several PEPs that mainly alter how type checkers work while making minimal other changes to Python, other than perhaps adding members to the typing module. Some examples from an article I wrote last year:

1 Like

I was considering this part of “core Python” :slight_smile:

The new PEP I’m drafting will have “immutable” TypedDicts, just not the TypedMapping protocol. It will look like

class Record(TypedDict, read_only=True):
    a: str

(Scare quotes around immutable because it’s only this structural type that lacks mutability. The underlying dict could still have other references that can change it.)