-
Notifications
You must be signed in to change notification settings - Fork 2
[RFC] Latest status of draft #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Thank you for writing this up! I'll admit I haven't really been following a lot of the discussion for intersection beyond messages in the typing bug. So I apologize in advance if any of this has been discussed already. First, I highly recommend that if this to become a PEP (which I expect it would), you should use the format of the PEP template in PEP 12: https://peps.python.org/pep-0012/#suggested-sections, and keep the section ordering. The current layout and ordering makes it hard to understand what is motivation vs specification vs other discussion. Second, I don't see a discussion of the rules with intersections with Third, I think the proposal needs to discuss changes to the language implementation, especially giving a name for the intersection type. While many (most?) people will probably use Finally, I think the section about value types and |
Most of this would fall under "rationale", with very little under "specification" and "implementation", the specification can be summed up as: "Intersections are a means of expressing the subtyping relationship: C ⊆ A ∩ B ⟺ (C ⊆ A ∧ C ⊆ B) and all rules for them in the type system follow from existing subtyping rules." The implementation is to be nearly identical to that of Union, with only differing names for symbols, and a different operator implemented. Reasoning through the implications and ensuring all type checkers can reach the same conclusions here is the majority of the proposal, as this will require type checkers to reach the same conclusions here for this to be a feature that is useful to library authors no matter which type checker their users use. While it can be reorganized under those headings, most of the existing headings would end up subheadings of rationale then, and I'm not sure how useful such an organization will be, but I'll see if there's a way to clear up which parts are for which audience. Type checkers need some information here that to users is almost certainly going to seem strictly academic.
The rules for Any apply the same as any other type, no special casing is required to handle Any, the reductions provided are just consequences of the above subtyping relationship. Hopefully the examples make this more clear, but that includes that intersections containing Any retain gradual behavior related to Any.
I believe that's covered under the section heading "Implementation", though I could specifically say typing.Intersection and types.IntersectionType rather than the wording there.
Agreed, this will need an example that highlights it. The type system shouldn't care about Annotated, only the type contained in it, but the recommendations there for those using Annotated to extend the type system beyond specification will ensure users end up with intuitive beahvior, and that current extensions that are essentially acting as refinement types are compatible with the set-theoretic understanding of refinement types, even without them being in the type system officially. An example of this is the use of Annotated in libraries like msgspec and pydantic to constrain values |
Thank you writing up the draft. Could you explain what you mean by (un)inhabited code? The draft is hard to read for me. So I am happily waiting for the not yet written parts, which make it more understandable for users not so deep in type theory :-) |
You also need an example with
Language seems fine. Theory section is very dense, but the audience for that section is type checker maintainers, so I think that's fine. the theory section could even just be an appendix of the pep since the audience for it is so narrow. |
@mikeshardmind thank you for working on this! Could you upload the draft on Google Docs or make it a PR so that it is easier for people to ask questions/leave comments? |
@DiscordLiz The example for total ordering I intend to include is something like this: class LT(Protocol):
def __lt__(self, other: object) -> bool: ...
class LE(Protocol):
def __le__(self, other: object) -> bool: ...
class GT(Protocol):
def __gt__(self, other: object) -> bool: ...
class GE(Protocol):
def __ge__(self, other: object) -> bool: ...
class EQ(Protocol):
def __eq__(self, other: object) -> bool: ...
class NE(Protocol):
def __ne__(self, other: object) -> bool: ...
type MinimumComparable = EQ & (LT | LE | GT | GE)
type TotalComparable = EQ & LT & LE & GT & GE & NE
def total_ordering[T: MinimumComparable](typ: type[T]) -> type[T &TotalComparable]: ... I'll grab a real world example of improved typed dict composition from a library that handles typed-json payloads. lru_cache is trivial (we've discussed that one before) I'm not sure I want an example with |
@CarliJoy See the section "Definitions", but it's essentially referring to a case where we can logically deduce that there are no possible runtime values, and that we've either exhausted all possible values (such as with exhaustiveness checking in match statements) or there is an error of some sort. |
I'm intending to move this forward to looking for a sponsor rather soon and am not currently looking for detailed line-by-line feedback, this is largely already the result of taking the time to distill down years of theory discussion to what we know to work, and what can be shown as working not just in python, but in other gradual type systems as well. Ever since the definition of "Any" has been ammended in such a way that it is compatible with set-theoretic intersections (Any was updated not be a catch all, but more accurately a compatible unknown, you can read more about this in the sections on Gradual Types and Materilization in the specification), it has been possible to define intersections building on set-theory without special casing of Any. I'll let you know when the RST form in standard PEP formatting is available if that's easier for you to comment on. |
This looks really good! A couple of things I noticed: In the trivial reductions section I think we should add: Perhaps also in the Re Also re negation, I think there are still quite a few outstanding questions so personally I would move it into the future work section. E.g. If a user has: class A:
foo: str
class B:
foo: int
bar: str What is |
It is
No, it has only the attributes of |
Ah ok this makes sense - thanks! I hadn't thought about using By this logic: class A:
foo: str
class B:
pass
|
No, but that's something that needs expanding on for any work on user-denotable negation. |
Another way to put this is that for any types If either
I don't think that's true. |
Sorry, that was poorly phrased. The existing subtyping rules don't work the way imagined here for being a subtype of a negated type, and requires strictly adopting the set-theoretic subtyping definitions, something which hasn't been fully done in python. What we know with ~A, is that if such a type exists, it must not be a subtype of A, but this alone says nothing about the type's other relationships, and as you pointed out, it's possible there there are types that are A & B, and these types aren't in ~A |
The typing spec has adopted the set-theoretic subtyping definitions as the intended semantics of Python subtyping. Current type checkers may not fully implement them in all cases, but those are type-checker limitations, not a reflection of the underlying theory of Python typing. Otherwise, agreed. |
I'm not sure if I fully track all the implications here re negation, but I would agree that it seems like something perhaps separating and saving for a future PEP! Any comments on these points? (pasting here for clarity)
|
I'm not sure how much we can escape specifying the behavior of negation types by saying they are not user-denotable. They still must exist (because |
We can leave some of this up to type checker interpretation to the extent that negation is disjoint from the original, which is the extent I'm specifying currently. This covers the TypeIs case for now, and leaves the right pathway for defining it in full for user direct use later |
This is my first open source comment, so excuse me if this sounds amateurish.
If I understand the above quote from the pdf correctly, it's saying that if any attribute in an Intersection type resolves to typing.Never, that Intersection type should be equivalent to typing.Never. It seems that objects with attributes of type Never in general aren't resolved to Never, atleast according to Pylance. The offical docs for typing.Never (type specification and module)) don't really mention that sort of resolution either. So treating intersections with any typing.Never attribute as typing.Never is thus inconsistent with how objects with typing.Never attributes aren't resolved to tyoing.Never. Considering that, how small or big of a deal is this inconsistency to the overall proposal? Will there need to be another separate proposal that addresses that? |
I guess you could draw a distinction between a defined class which has been annotated explicitly with In the case of intersections, if an intersection produces |
There's no inconsistency here, pyright and other type checkers currently don't handle Never as a value correctly (something with a type of Never being assumed to have a value at runtime should always be an error). As for another proposal, I am attempting to specify this separately, and need to update the draft to mention this when I do my next editing pass on it. feel free to see this thread: https://discuss.python.org/t/better-specification-for-when-the-presence-of-typing-never-should-be-considered-a-type-system-error/89757 |
I think that it is a mistake to conflate "existence of an attribute" and "type of an attribute". An annotation of The correct resolution of the "inconsistency" is simply that the current behavior is wrong (or at least, incomplete.) |
considering how the discussion about related topics has gone, I'm gonna step away from further work on this. I don't think the ecosystem is ready for it, and I don't enjoy the way discussion was stifled by repeatedly misrepresenting an argument until moderators got involved. |
after over 10 years of bikeshedding while typescript just did things, i'm so glad this is finally getting proposed. my only feedback is: don't let "perfection" get in the way of "anything." if there's an intractable corner case blocker, just leave it "undefined behavior TBD" in v1. that's OK. a later PEP can resolve it. after 10 years of pip-tier bikeshedding, this cannot come fast enough. propose away 🫡 |
The remaining issues aren't corner cases, or I would have already put the draft up on discourse and been looking for a sponsor. The number of people pushing for it to be rushed without understanding that there are major open questions to still be resolved is part of why I haven't posted it on discourse. It's hard enough for people to focus on the smaller pieces to the point of often feeling like people may not have the requisite background knowledge in how they chime in. (I want to be clear here that if people have concerns and they don't understand if/how their concerns interact, please still comment, but there may be gaps in what I have well-thought out explanations for that aren't as heavily reliant on theory at this point still) For one thing, while the definition I've proposed for intersections doesn't violate the gradual guarantee, it's been pointed out that type checkers are already violating the gradual guarantee for intersections via TypeIs, which means it won't be sufficient to just use the intersection implementation type checkers have for internal use currently. This has to be corrected and agreed upon. This was one of my objections to TypeIs being accepted having language about intersections, and now we're facing the long-tail consequences of "just do it, but leave the specification sparse and important questions unanswered" (and to be clear, I understand that there were also benefits to that, but now we have to take the time to clear things up) Typescript's intersections also don't follow the rules of python's type system, so "just doing it" the way they do, isn't going to result in good outcomes. |
One reason I wanted to have an intersection type, was to support this use case: import strawberry
@strawberry.type
class User:
name
# Now the User class has a `__strawberry_definition__` property
# or, better, its type would be `User & StrawberryType`
|
I'll add an explicit note in the next iteration of the pep draft that this is intended to work (dataclass transform is technically a special case itself, so clarifying that the intent is not to special case intersections further within it), but as far as I'm concerned, it requires no special casing and was just automatic from other changes. It would be going from: @typing.dataclass_transform()
def type(cls: Type[_T]) -> Type[_T]: # no knowledge of strawberry-specific attributes
... to class StrawberryTypeAdditions(typing.Protocol):
...
@typing.dataclass_transform()
def type(cls: Type[_T]) -> Type[_T & StrawberryTypeAdditions]:
... |
@mikeshardmind that's awesome! I don't seen any mention in the PDF about typed dictionaries? Is that in scope for this PEP or not? (I guess it should be at least mentioned in that case) |
Also something I think follows automatically from the subtyping rules, though needs expanding on in examples when they are added, as well as noting which things here won't work and may require future extensions to typed dicts/structural types. I commented a bit on this in the discussion for the PEP on inlined type dictionaries, but I broadly support ensuring the ergonomic tools to work with structural types are added as needed, and think they can be proposed independently of intersections, though both intersections and some of the other possible extensions would both be more useful when having access to both at once. |
Uh oh!
There was an error while loading. Please reload this page.
This is in pdf form for now because it was easier to work with locally. I'll have to translate this over to rst for the proposal process, and will do so soon.
I consider this almost ready to propose as-is. I'm looking for comments on which examples we should use (we have too many to use all of them) for the motivation section and whether any of the language remains unclear.
2025-4-18-draft2.pdf
The scope has been narrowed down a bit, and sections have been re-ordered, re-worded, and clarified thanks to the first round of feedback received in other channels from @carljm @Jackenmen @DiscordLiz and @Sachaa-Thanasius
This incorporates all of the collected theory that we had agreement for, limits the definition of negation in such a way that user-denotable negation can be deferred to a later proposal, and avoids breaking the gradual guarantee.
Included alongside the theory are a procedural set of steps that are equivalent, yet that do not require set-theoretic type checker internal representations, and minimizes both false positives and false negatives to the minimum possible without breaking the gradual guarantee.
Not yet written:
The first of those is a blocker for the proposal to be made formally; the other two are not, but will still need to be done as part of the process if this is to be accepted.
The text was updated successfully, but these errors were encountered: