Repository navigation
Closed
Description
There is a great deal of confusion about handling the Any type within an Intersection.
In Python, Any is both a top type (a supertype of all types), and a bottom type (a subtype of all types). Python has a gradual typing system, meaning that it will never be required that everything is typed. Everything that is not typed is considered Any.
We examine five ways to handle intersections with Any:
Anyis removed from intersections:T & Any = T.- Intersections containing
AnybecomeAny:T & Any = Any. Anyis forbidden in intersections:T & Anyis an error.Anyis not reduced within intersections.Anyis only considered in an intersection in deference to non-gradual types.
Remove Any from Intersections
Arguments in favour
- Suggested by PEP 483.
Arguments against
- @CarliJoy shows an example where an interface disappears.
- @ippeiukai shows a similar a example where an interface disappears.
- @erictraut argues that there would need to be other special cases were this accepted.
- @erictraut analyzes this case.
An Intersection containing Any becomes Any
Arguments in favour
- @antonagestam mentions that TypeScript does this, although he finds it surprising.
- @ippeiukai argues that this is consistent with PEP 483.
Arguments against
- @mikeshardmind argues that this reduces the effectiveness of gradual typing by discarding constraints.
Disallow Any in Intersection
Arguments in favour
- @mikeshardmind argues that it is ambiguous.
- @mikeshardmind argues that "it is possible that
Any & Tis inherently unsafe".
Arguments against
- @randolf-scholz argues that they can arise from sequential instance-checks.
- @randolf-scholz argues that code could "synthesize an intersection type dynamically".
- @erictraut argues that "intersections with
Anywill arise, often in the bowels of the type checker's logic where there's no good way to report an error to the user".
Treat T & Any as irreducible in general
Arguments in favour
- @NeilGirdhar argues that changing a type annotation from
TtoAnyshould not cause type errors to appear, and by examining supertypes and interfaces. - @randolf-scholz argues from the various definitions of
Any. - @JelleZijlstra reasons about setting and getting a variable.
- @erictraut reasons by duality with union.
- @mikeshardmind reasons by subtyping in a related thread.
- @mniip argues by interpreting
Anyas a wildcard in gradual typing. - @kmillikin seems to agree.
Arguments against
- at least @CarliJoy has no idea how that should work in the reference, how should a type checker handle it in detail?
- @DiscordLiz and @mikeshardmind each argued that functionally this has all the same issues as treating it as Any
Any is only considered in an intersection in deference to non-gradual types.
Arguments for
- @mikeshardmind reasoned that this prevents gradual typing from becoming gradual untyping due to intersection being a widening of interfaces and that in cases where the wider type is desired, it is still directly expressible but requires being explicit about it.
Arguments against
- @NeilGirdhar argues that it breaks logical consistency with unions, which are the dual of intersections.
⚠️ Rules for contribution to this Issue
- Do not post, if your argument is already handled within the description
- Posts should include include a proper header i.e.
## Arguments in favour to Disallow Any in Intersection - If you want to correct or specify things in the description write a comment in the format "Old" > "New"
The general idea is that I will update the description, allowing the discussion to be included in the PEP and prevent a discussion going in circles.
I will react with 🚀 once I included something in the description.
Reacted by Tyler C Laprade, CFA, Neil Girdhar and Randolf Scholz
Metadata
Metadata
Assignees
Labels
No labels
Activity
mikeshardmind commentedon Jul 23, 2023
Arguments in favor of Removing Any from Intersections
Any is essentially the universal set of types. The identity element of intersection on sets is the universal set for domains in which a universal set does exist. The Identity element for unions is the empty set (conceptually, typing.Never) The divergence in behavior is easily explainable as a consequence of logical ordering, and can be taught easily utilizing python builtins
anyandalland their corresponding behavior, as well as the corresponding set operations.Edit: Argument retracted, Any's definitional mismatch between
?andΩdoes matter here.mikeshardmind commentedon Jul 23, 2023
Arguments against An Intersection containing Any becomes Any
This reduces the effectiveness of gradual typing, as it causes a known constraint of the intersection to be thrown away, causing Any existence of Any to become contagion to code using intersections. This could further fragment the ecosystem between fully typed code and not.
mikeshardmind commentedon Jul 23, 2023
Arguments in favour to Disallow Any in Intersection
It is possible that Any & T is inherently unsafe.
Consider the case
Any & AsyncCallbackwhere AsyncCallback is a protocol or abstract base class describing the expected interface a coroutine will be scheduled with. It is possible that a type that satisfies Any conflicts with the required interface of AsyncCallback. (This can also be seen as a reason to remove Any and only treat Any as the universal set, rather than disallow)antonagestam commentedon Jul 23, 2023
I think it's worth mentioning that TypeScript considers
T & any = any, though I find this surprising. Perhaps there have been previous discussions there that can be learned from?I think it's also worth mentioning that the description of Any in the original post here is incomplete. It is both a subtype and a supertype of all other types.
NeilGirdhar commentedon Jul 24, 2023
Dear everyone.
I'd like to propose a fourth option, which is that
T&Anycollapses toAnyin some situations, collapses toTin others, and is irreducible in general. I organized this post in a few sections:Any, andI briefly discussed this with Jelle who suggested I post this to this thread. Please let me know if I've made any errors.
Background: Unions with
AnyBefore I get into the meat of it, here's an interesting discrepancy between PyRight and MyPy:
As we can see, Pyright is not collapsing
str | AnytoAny. It treats it as irreducible.I think MyPy is doing the wrong thing here. (I discussed this with Jelle who said it was due to "mypy's general behavior to use the join operator for ternary expressions. We'd like to fix that at some point but it's difficult for compatibility reasons.")
The reason this is incorrect is for the same reason it's an error to do this:
Thus, we can see that in general
T | Anyis notAny. Although, there are cases where it can be collapsed toAny.Background: Definition of
AnyIt's also worth exploring what
Anyreally means since that seems to be a source of confusion.Anyis not:object,Omega = T_1 | T_2 | ...for all typesT_i.That's because its supertypes
Sup(Any) = AnywhereasSup(Omega) = ∩_i Sup(T_i) = Sup(object) = object.This is why we cannot reason from set theory.
Analysis
For any type
T, consider its subtypesSub(T)and supertypesSup(T). For an intersection,A & B, we have:Sub(A & B) = Sub(A) ∩ Sub(B)This means:
Sup(A & B) = Sup(A) ∪ Sup(B)This means:
So whereas it is true that each component of the intersection narrows the set of
Sub(A&B), it increases the setSup(A&B). Intuitively, this means that each element is an extra constraint on what you can pass to a function that accepts an intersection, but is a broader interface of what you can do with a variable of typeA&B.We can now deduce the rules:
Sub(A&C) = Sub(A) ∩ Sub(C)Sup(A&C) = Sup(A) ∪ Sup(C)For ordinary types
AandC<A, then we get:Sub(A&C) = Sub(C)Sup(A&C) = Sup(C).And thus, the regular rule that
A&C=Cis true for ordinary types.However, this is not the case when
CisAny! Then we haveSub(A&Any) = Sub(A) ∩ Sub(Any) = Sub(A)Sup(A&Any) = Sup(A) ∪ Sup(Any) = Sup(Any) = Omega!Thus, in general,
A&Anyis an irreducible form.But as a parameter of a function (or similar subtype question), we have
As a parameter to a function (or similar supertype question), we have
Why does intersection broaden the way a variable can be used?
This seems paradoxical, but intersection broadens the way a variable can be used in exactly the same way that its dual, union, narrows the way a variable can be used.
Consider:
Given a union, only those operations that can be done to every element of the union can be done to the union.
This is why any operation that can be done to any element of the intersection can be done to the whole intersection. We can test this in Pyright:
Thus, the intersection
A&Bcan do anything that any of its components can do.Consequences
In a function, the desired behavior of collapsing
Anyis correct:(This is what everyone wants.)
If a variable ends up with type
T & Any, for example as the output of a generic function, it should be usable everywhere:We need some kind of special case for
isinstance(x, T)whenxhas typeAny.I think the easiest thing to do would be for isinstance to intersect types unless one type is
Any, in which case it replaces it. Some thought should go to this to ensure that no weird cases pop up. In the worst case, a new parameter to isinstance may be necessary to select intersection or replacement. This addresses the "contagion" effect mentioned above. It also means thatT & Anywill be extremely rare, which I think is what everyone wants as well 😄.Along the same lines, there are other ways to narrow types, including
singledispatch,matchstatements, and function overloads. I think the logical thing to do is to allowAny & Tto leak into them and out of them. If this becomes problematic, a user may be able to useisinstanceto eliminateAny.Danger of unconditionally collapsing
T & AnytoTConsider:
If intersection collapses types automatically, then
z.f()fails even though all that's changed betweenyandzis that the argument toghas been changed from a type toAny. It would be extremely weird for broadening a type toAnyto create type errors! These errors can be very hard to track down since they can be totally unlocalized (one library can change a type toAny, and the error can pop up miles away in user code that doesn't even use that function directly). Thus, these errors would be extremely pernicious.Conclusion
T & Anyis irreducible in general. Its subtypes are the subtypes ofT, and its supertypes are the universe of all types.mikeshardmind commentedon Jul 24, 2023
I entirely disagree with the above.
Anyis the universal set of python objects andobjectis not. Not only is this the actual definition of Any per pep 484, butobjectis clearly not the universal set of objects, as the type of types istypeWhile having a problematic defined interface.The current definition of Any itself creates contradictions in any attempt at a logically consistent type system
The attempt to treat Any as if it is a subtype or supertype is misguided at best and leads to inconsistent results.
While Any is implemented as a type in python, it isn't conceptually a type, but the set of all types when it comes to how it is treated in type checking and it's purpose in the type system.
Additionally, the claim that intersection should work like Union in the below is a complete misunderstanding of logical operators
Intersection is "all" requirements, Union is "Any" requirements. Intersection should not be seen as broadening the allowable types, that is what Union does. Intersection is more restrictive on the types allowed the more types which are added to it.
Conflating the potential interface with the allowable types is problematic. In the Case of
A & AnyThings which are notAstrictly are not part of the intersection.The other problem comes in from the other part of the definition of Any per pep 484
This speaks to the potential interface of Any, not to the type of Any. Without changing the definition of Any, this part should make Any unsafe as an operand in an intersection, as by having all potential methods, it also has all potential conflicting method definitions with other operands of the intersection.
consider:
These are incompatible interfaces for an intersection and Any can be either.
NeilGirdhar commentedon Jul 24, 2023
Where do you see in PEP 484 that
Anyis the universal set? Perhaps it would be best to clarify using mathematical language exactly what you mean by "universal set". It's not the union of all typesOmegafor the reason I described above. Do you mean something else?I didn't say
objectwas the universal set. I said that object is the universal base class. This is a technical term. I'll add a link to the wikipedia article.Intersection broadens the allowable parameter types for which
xcan be passed for the exact same reason that union narrows it. I will add a section on a comparison to unions then to make all of the rules clearer and more obvious. Since unions are implemented today, you will be able to verify these rules yourself.mikeshardmind commentedon Jul 24, 2023
This is entirely wrong, you are conflating types and their interfaces here, as was already explained to you in the main thread here: python/typing#213 (comment)
Except that it isn't, you can see this with the type of
objectbeingtype, and the type ofobject()beingobject, I'm quite aware of how the type system works and don't need you to link me a Wikipedia article on the matter.I've edited in more detail. The term universal set of objects is not used, but definitions consistent with it are. However there's another part of the definition which is problematic by both saying
Anyis consistent with all objects, and also sayingAnyhas all interfaces. This is quite literally self-contradictory the moment we consider that interfaces can have multiple conflicting definitions.NeilGirdhar commentedon Jul 24, 2023
I think the fundamental disagreement here is that you are identifying a type by the set of subtypes only
Sub(X)? The inferface (Sup(X)) is also important, and it's affected by union and intersection. This is whyX | Anyis irreducible in Pyright today.I think it's fairly well accepted that
objectis the universal base class sinceisinstance(x, object)is true for allx, which is the definition.Normally, in mathematics, the universal set is the union of all elements.
Anyis not union of all typesOmega, so I assume you mean something different.I don't think this is problematic. This is why I prefer to consider to type expressions X and Y to be equivalent only when
Sup(X) == Sup(Y)andSub(X) == Sub(Y).I think at heart, the main point of contention is that are thinking about intersection only in parameter lists or similar subtype questions. In those situations, you are right that you can reduce
T & AnytoT. However, those are not the only situations in which intersections pop up. For example, see the consequences example I gave. Similarly, unionsT | Anydo reduce toAnyin parameter lists and other subtyping questions, and similarly, they are irreducible in other situations (see the example in the background section).So, I'm not contradicting you in those parameter list situations. However, there are other situations to consider, and it is possible for a revealed to type to meaningfully have type
T & Any, which would be irreducible.mikeshardmind commentedon Jul 24, 2023
your edit adds "Why does intersection broaden the way a variable can be used?" this is the definition of an interface, not a type. You are still conflating the two, and the difference between types and their interfaces is actually why this is such a problem with the current definition of Any with intersections. Please review what other people have told you about this.
Unions are broadening of type, but only the shared interfaces may be safely used because we only know that it has at least one of these interfaces.
Intersections are narrowing of type, but because of the requirement of compatibility with all of an intersection's types, allow using the combined interface of the types.
In any interpretation of Any based on its current definition in accepted peps, Any presents a potential problem here.
Any having all potential interfaces makes
Anymeans that all of the potential interfaces of Any are in conflict with the other operands as they can be defined as incompatible, it has all interfaces, compatible versions of them and not. For this reason, either the definition of Any needs changing or Any is unsafe as an intersection operand.But even with this, the other type requirement of the intersection does not go away in the process.
The definition of Any is logically inconsistent and intersection and its logical consequences just shine a light on this pre-existing problem.
For what it is worth, mypy provides configuration options forbidding the use of Any specifically because of the problems of Any, and Ruff provides lints disallowing Any for the same reasons.
If it comes between implementing a typing feature in a way that is logically broken, and not allowing some code that already isn't typed to make use of a typing feature, the latter is preferable.
NeilGirdhar commentedon Jul 24, 2023
When we talk about types, they can occur in a variety of places. One of those places is that they can be associated with a variable as a "static type". These static types must encode all of the information that we have about the static type, including the interface (the way in which the variable can be used).
This discussion is about how type checkers should manipulate intersections with static types. Because the interface matters, for the static type of variables, you cannot collapse
T&AnytoT.Ultimately, type checkers have to report the static types with
reveal_type. And so the types returned by reveal-type will sometimes reportT&Any, which is distinct fromT.Maybe for you a type checker's "static type" is not a "type". But this is one sense in which I am discussing types.
Another place types come up is in parameter lists. In that location,
T&Anydoes collapse toT. But type checkers deal with types in a variety of places—not just parameter lists.That's not a conflict for the same reason that
T | Anyis not a "conflict". You can pass whatever you want to a function that accepts this type.To be honest, I don't think anything is logically broken.
mikeshardmind commentedon Jul 24, 2023
It is a conflict in the intersection case though.
T & Anyrequires the interfaces of both T and Any. Any has all interfaces, those compatible with T and not.You can't do whatever you want with T | Any
This also isn't unique to parameters, as the type also matters when it comes to potential reassignment, it happens even with a direct typing of a variable:
You can only use the interface provided by T safely, as you could receive T here, rather than Any. While Any has a compatible interface, you still need to care about the interface of T.
The difference between types and their interfaces matters. you cannot handwave them away with less rigorous definitions, and this is highly important here because the behavior of Union and Intersection are diametrically opposed in their effects of broadening vs narrowing of both interfaces and types
The logical contradiction has already been presented, and you have not given an explanation which satisfactorily shows it isn't in contradiction.
NeilGirdhar commentedon Jul 24, 2023
Sorry, I meant the dual of "pass whatever you want". You can do whatever you want with
T & Anythe same way that you can pass whatever you want to a function acceptingT | Any.I think we're going in circles again. The static type includes all of the information known about the type. You seem to be dividing this information into something you're calling the "type" and the "interface". I think your definitions roughly correspond to Sub(X) and Sup(X), but it's hard to say. Nevertheless, when I use the word "type", I mean the static type (as returned by reveal type). The static type must encode what you call the "interface"—it is not distinct.
Just what I said above: "You can do whatever you want with
T & Anythe same way that you can pass whatever you want forT | Any." There's no contradiction. Why don't you show some code that is ambiguous?DiscordLiz commentedon Jul 24, 2023
I don't know why you are still arguing this @NeilGirdhar but type theory is pretty clear here and you keep asking for things you've already been provided by people being far too patient with you.
NeilGirdhar commentedon Jul 24, 2023
I only posted because Jelle looked my post over and said it looked right. Let's try to keep the discussion civil.
269 remaining items
mark-todd commentedon Dec 19, 2023
Yeah I think it would be helpful to hear from them about option 5 - wasn't sure what to write in the summary as it didn't exist back then, but I'll try and add them a row (I've added None as an option for no opinion known).
mikeshardmind commentedon Dec 19, 2023
I'm gonna step away for a bit I may look at this later, but I'm getting tired of having to repeatedly state things and having portions of it quoted out of context, or important details that are inconvenient to someone else being left out in responses. I can understand that some people may not be happy with some of the options, but that's not a reason to completely ignore the arguments being made and ignore details that have directly presented, and this has been a recurring interaction over the course of months.
mark-todd commentedon Dec 19, 2023
I do really feel like we're this close to reaching a consensus - there are really only a few edge cases remaining. There is a lot of discussion on this issue and holding all the points and counterpoints in your head at once is difficult, at least for me. I would suggest we try and conclude this issue - then fleshing out the details of the PEP we can figure out in the new year. We work under the assumption that we're going with option 5, but then try and deal with the specific edge cases that might generate false positives etc.
NeilGirdhar commentedon Dec 19, 2023
I think you really need to get the buy-in of MyPy and Pyright at the least. After all, they are going to review the PEP and their feedback will make or break its passing. I don't think it makes sense to write it up only for it to get rejected. By all means, make the best case as to why 5 is better than 4 if that's what you feel strongly about. I suggest starting by adding links to positive arguments for 5 in this issue header to make it easy to follow.
NeilGirdhar commentedon Dec 19, 2023
As I said before, I believe that behavior is a consequence of not having intersections already. Do you have a reference for an alternative explanation?
DiscordLiz commentedon Dec 19, 2023
@NeilGirdhar Maybe try reading what you said you did.
mark-todd commentedon Dec 19, 2023
Yeah I do agree - I think I'll try and prepare a document that compares them, although I'd appreciate @mikeshardmind 's input on that one. If not, I will try and gather what I can from the discussions. Then maybe @gvanrossum is there some way you could bring it to them for review?
DiscordLiz commentedon Dec 19, 2023
Or here since you're so averse to that:
That part precludes an intersection, as that would no longer tell you "if you get it wrong" for the motivating case.
NeilGirdhar commentedon Dec 19, 2023
Great idea. You can draw from the top level sections of this issue. I'll try to fill in some arguments for and against if I can.
erictraut commentedon Dec 19, 2023
Could someone point me to a full description of options 4 and 5? I'm struggling to understand the nuanced differences between these two. I see some examples that try to demonstrate where the two differ, which is helpful, but I'm looking for a clear articulation of the rules — in particular, how they differ and whether they are fully symmetric with unions.
FWIW, here are my primary criteria for evaluating any proposal:
Any,Callable[..., Any], etc.? Ideally, there should be no special cases required, and the behavior should fall out of the composition of the basic rules.mark-todd commentedon Dec 19, 2023
Thanks for this. It took me a while to understand 4 vs 5, but this point helped:
#1 (comment)
I think the only special case is Any? We're currently working on a draft of something to compare them
DiscordLiz commentedon Dec 19, 2023
@erictraut
@mikeshardmind would probably be the best one to make the argument for 5 over 4, but he's stepped away for now, and reiterated in other places that he is not going to continue the line of discussion today.
Under proposal 5
Under proposal 4
That depends on if we consider the subtyping behavior we currently have with Any as a basic axiom. It's already a special case. If we allow the same behavior that already exists in the special case, option 5 is the only option which remains fully consistent with all existing prior decisions. If we do not, then option 4 is inconsistent with existing behavior, but follows entirely from basic axioms. "Nothing newly special" was one of the critera he had when designing option 5
As I understood him to mean it, the reasoning for option 5 follows here:
The comments linked by @mark-todd help, one of mine may help as well: #31 (comment) , as well as his[@mikeshardmind ] response to that #31 (comment)
CarliJoy commentedon Dec 19, 2023
Hey everybody. I will lock this discussion.
This issues has awy too many comments. Reading it is not easily possible anymore.
We are anyhow IMHO just discussing between Option 4 and 5 and for this #31 exists.