TL;DR: AnyOf us neither Union nor Intersection -- it behaves like one or the other depending on whether it's in a "giving" or "receiving" position. This makes it convenient (but unsound). On Wed, Sep 23, 2020 at 9:19 AM Anthony Sottile <asottile@umich.edu> wrote:
[...] It's still unsound, but it's a strict improvement over Any
I guess for clarity in definition: - Union[T1, T2] only allows attributes which are the _intersection_ of those provided by T1 & T2 - AnyOf[T1, T2] allows attributes which are the _union_ of those provided by T1 | T2
Thinking about it more, maybe AnyOf should not be the same as Intersection. A key idea is that types have two roles: "receiving" and "giving". E.g. consider ``` def fun(arg: A) -> R: return ... ``` 1. Union From the caller's perspective, `arg: A` is a receiving position; if A is X|Y (using the new PEP 604 notation for unions) then we can pass either an X or a Y (or something if type X|Y), which is "easy" (by which I mean it's easy to satisfy the type checker). And the caller is receiving R. If R is a union, the caller has to discriminate the union somehow (typically using isinstance(); or using `if r is None` if R has the form X|None). I will call the requirement to discriminate "hard" (because it requires extra code that for various reasons often isn't there in code that was originally untyped). But from the perspective of the callee the roles are reversed: if A is X|Y then it has to discriminate arg ("hard"), whereas if R is X|Y, foo can return either an X or a Y indiscriminately ("easy"). Note that hard and easy are swapped compared to the caller's perspective (I'm sure this relates to contravariance somehow :-). None of that is news. 2. Intersection If we had classic intersection types spelled as X&Y, from the caller's perspective, if A is X&Y, we have to provide something that is both an X and a Y (perhaps involving multiple inheritance), which I consider "hard", whereas if R is X&Y, we can just use it as if it is both an X and a Y ("easy"). (From the docs Eric pointed to, it looks like JavaScript's Intersection type can be seen as multiple inheritance from two TypedDicts.) For the callee, the roles of intersection types are again reversed -- `arg: X&Y` is "easy", while `-> X&Y` means we have to work "hard" at constructing something that's both X and Y. Again, hard and easy are swapped compared to the caller -- and they are also swapped compared to Union types (a Union arg is hard for the function body, while an Intersection return is hard for it). That should also not be news, if you see Intersection as the "opposite" of Union. (It's less useful mostly because constructing something that's both an X and a Y is even more of a pain than discriminating a union, special cases for TypedDict notwithstanding.) 3. AnyOf Now we get to the kicker. A function that returns AnyOf[X, Y] makes it "easy" for the caller -- at least, easy to satisfy the type checker. So it behaves like an Intersection in a "giving" position. But it should be easy for the callee too! You can return either an X or a Y to make the checker happy, so here it behaves like a union. And from this we derive that AnyOf types are easy in all positions. 4. Afterthoughts Why is AnyOf so convenient? It gives up soundness for convenience, without degenerating entirely to Any. The behavior is similar to Any, because it is both a local top type and a local bottom type, if those concepts make sense. -- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>