Skip to content
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

Introduce an Intersection #213

Open
ilevkivskyi opened this issue May 6, 2016 · 236 comments
Open

Introduce an Intersection #213

ilevkivskyi opened this issue May 6, 2016 · 236 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@ilevkivskyi
Copy link
Member

This question has already been discussed in #18 long time ago, but now I stumble on this in a practical question: How to annotate something that subclasses two ABC's. Currently, a workaround is to introduce a "mix" class:

from typing import Iterable, Container

class IterableContainer(Iterable[int], Container[int]):
    ...

def f(x: IterableContainer) -> None: ...

class Test(IterableContainer):
    def __iter__(self): ...
    def __contains__(self, item: int) -> bool: ...

f(Test())

but mypy complains about this

error: Argument 1 of "__contains__" incompatible with supertype "Container"

But then I have found this code snippet in #18

def assertIn(item: T, thing: Intersection[Iterable[T], Container[T]]) -> None:
    if item not in thing:
        # Debug output
        for it in thing:
            print(it)

Which is exactly what I want, and it is also much cleaner than introducing an auxiliary "mix" class. Maybe then introducing Intersection is a good idea, @JukkaL is it easy to implement it in mypy?

@JukkaL
Copy link
Contributor

JukkaL commented May 6, 2016

Mypy complains about your code because __contains__ should accept an argument of type object. It's debatable whether this is the right thing to do, but that's how it's specified in typeshed, and it allows Container to be covariant.

I'm worried that intersection types would be tricky to implement in mypy, though conceptually it should be feasible. I'd prefer supporting structural subtyping / protocols -- they would support your use case, as IterableContainer could be defined as a protocol (the final syntax might be different):

from typing import Iterable, Container

class IterableContainer(Iterable[int], Container[int], Protocol):
    ...

def f(x: IterableContainer) -> None: ...

class Test:
    def __iter__(self): ...
    def __contains__(self, item: int) -> bool: ...

f(Test())  # should be fine (except for the __contains__ argument type bit)

@ilevkivskyi
Copy link
Member Author

It would be really cool to implement protocols. Still, in this case intersection could be added as a "syntactic sugar", since there would be a certain asymmetry: Assume you want a type alias for something that implements either protocol, then you write:
IterableOrContainer = Union[Iterable[int], Container[int]]
But if you want a type alias for something that implements both, you would write:
class IterableContainer(Iterable[int], Container[int], Protocol): ...
I imagine such asymmetry could confuse a novice. Intersection could then be added (very roughly) as:

class _Intersection:
    def __getitem__(self, bases):
        full_bases = bases+(Protocol,)
        class Inter(*full_bases): ...
        return Inter

Intersection = _Intersection()

then one could write:
IterableContainer = Intersection[Iterable[int], Container[int]]

@JukkaL
Copy link
Contributor

JukkaL commented May 6, 2016

Intersection[...] gets tricky once you consider type variables, callable types and all the other more special types as items. An intersection type that only supports protocols would be too special purpose to include, as it's not even clear how useful protocols would be.

@ilevkivskyi
Copy link
Member Author

I understand what you mean. That could be indeed tricky in general case.

Concerning protocols, I think structural subtyping would be quite natural for Python users, but only practice could show whether it will be useful. I think it will be useful.

@gvanrossum
Copy link
Member

This keeps coming up, in particular when people have code that they want to support both sets and sequences -- there is no good common type, and many people believe Iterable is the solution, but it isn't (it doesn't support __len__).

@ilevkivskyi
Copy link
Member Author

I think Intersection is a very natural thing (at least if one thinks about types as sets, as I usually do). Also, it naturally appears when one wants to support several ABCs/interfaces/protocols.

I don't think that one needs to choose between protocols and Intersection, on the contrary they will work very well in combination. For example, if one wants to have something that supports either "old-style" reversible protocol (i.e. has __len__ and __iter__ methods) or "new-style" (3.6+) reversible protocol (i.e. has __reversed__ method), then the corresponding type is Union[Reversible, Intersection[Sized, Iterable]].

It is easy to add Intersection to PEP 484 (it is already mentioned in PEP 483) and to typing.py, the more difficult part is to implement it in mypy (although @JukkaL mentioned this is feasible).

@gvanrossum
Copy link
Member

For cross-reference from #2702, this would be useful for type variables, e.g. T = TypeVar('T', bound=Intersection[t1, t2]).

@jeffkaufman
Copy link

Intersection[FooClass, BarMixin] is something I found myself missing today

@matthiaskramm
Copy link
Contributor

If we had an intersection class in typing.py, what would we call it?

Intersection is linguistically symmetric with Union, but it's also rather long.
Intersect is shorter, but it's a verb. Meet is the type-theoretic version and also nice and short, but, again, you'd expect Union to be called Join if you call Intersection Meet.

@jeffkaufman
Copy link

As a data point, I first looked for Intersection in the docs.

@ilevkivskyi
Copy link
Member Author

Just as a random idea I was thinking about All (it would be more clear if Union would be called Any, but that name is already taken). In general, I don't think long name is a big issue, I have seen people writing from typing import Optional as Opt or even Optional as O depending on their taste. Also generic aliases help in such cases:

T = TypeVar('T')
CBack = Optional[Callable[[T], None]]

def process(data: bytes, on_error: CBack[bytes]) -> None:
    ...

@mitar
Copy link
Contributor

mitar commented Oct 18, 2017

I just opened #483 hoping for exactly the same thing. I literally named it the same. I would be all for Intersection or All to allow to require a list of base classes.

@ilevkivskyi
Copy link
Member Author

Requests for Intersection appear here and there, maybe we should go ahead and support it in mypy? It can be first put in mypy_extensions or typing_extensions. It is a large piece of work, but should not be too hard. @JukkaL @gvanrossum what do you think?

@gvanrossum
Copy link
Member

gvanrossum commented Oct 21, 2017 via email

@ilevkivskyi
Copy link
Member Author

ilevkivskyi commented Oct 22, 2017

@gvanrossum

I think we should note the use cases but not act on it immediately -- there are other tasks that IMO are more important.

OK, I will focus now on PEP 560 plus related changes to typing. Then later we can get back to Intersection, this can be a separate (mini-) PEP if necessary.

Btw, looking at the milestone "PEP 484 finalization", there are two important issues that need to be fixed soon: #208 (str/bytes/unicode) and #253 (semantics of @overload). The second will probably require some help from @JukkaL.

@JukkaL
Copy link
Contributor

JukkaL commented Oct 23, 2017

I agree that now's not the right time to add intersection types, but it may make sense later.

@Kentzo
Copy link

Kentzo commented Nov 14, 2017

(been redirected here from the mailing list)

I think the Not type needs to be added in addition to Intersection:

Intersection[Any, Not[None]]

Would mean anything but None.

@rinarakaki
Copy link

How about the expression Type1 | Type2 and Type1 & Type2 alternative to Union and Intersection respectively.

example:

x: int & Const = 42

@ethanhs
Copy link
Contributor

ethanhs commented Nov 15, 2017

@rnarkk these have already been proposed many times, but have not been accepted.

@JelleZijlstra
Copy link
Member

JelleZijlstra commented Nov 15, 2017

The Not special form hasn't been proposed before to my knowledge. I suppose you could equivalently propose a Difference[Any, None] type.

What's the use case for that? It's not something I've ever missed in a medium-sized typed codebase at work and in lots of work on typeshed.

@mikeshardmind
Copy link

mikeshardmind commented Jul 22, 2023

Do you accept that f is in Any's interface?

No. I accept that there could be infinitely many types that satisfy Any, but that's different from saying that those infinitely many types have a known interface known to contain f

The definition of an intersection here is that things that satisfy the intersection are the things that satisfy the types of all of the member types or protocols of the intersection. Where each portion comes from is irrelevant to this.

T & Any isn't satisfied by an unrelated Q, because Q doesn't satisfy T. The requirement of satisfying T is why it doesn't reduce to Any, the fiction that Any could potentially contain every interface which effectively disables type checking with some settings is irrelevant to satisfying the requirement of T

@NeilGirdhar
Copy link

NeilGirdhar commented Jul 22, 2023

No. I accept that there could be infinitely many types that satisfy Any, but that's different from saying that those infinitely many types have a known interface known to contain f

I don't understand. If f is not part of Any's interface, why does this pass type checking?

def function(x: Any):
    x.f()

No type checker checks (or is supposed to check) whether "those infinitely many types have a known interface known to contain f". Any essentially makes all type checks pass.

I think this is where the misunderstanding is happening.

The definition of an intersection here is that things that satisfy the intersection are the things that satisfy the types of all of the member types or protocols of the intersection.

Please let's stick to the examples. We obviously disagree on intuitions.

@mikeshardmind
Copy link

Okay, let me put this in very simple terms

Any is always satisfied by an object
T may or may not be satisfied by an object

The requirement to satisfy the requirements of Any can be removed as trivial, as they are always satisfied, but an item being checked against an intersection has to be a member of each component of the intersection (this is the entire rationale for an intersection, and the definition matches with the proposal above)

In set theory terms, Any is the set of all types

Given a type specification T, and instance of an object P, P is a member of the intersection of T and Any if and only if P is a member of T

@NeilGirdhar
Copy link

Given a type specification T, and instance of an object P, P is a member of the intersection of T and Any if and only if P is a member of T

The problem is that you are confusing the universal base class object with the other top type Any.

It is true that every type is compatible with both object and Any. However, it is also true that Any is compatible with every type. And it's this last rule that is why you cannot remove Any from intersections—for the same reason that you cannot remove any other arbitrary type from intersections.

The best way to analyze these problems is to focus on examples.

@mikeshardmind
Copy link

mikeshardmind commented Jul 22, 2023

It is true that every type is compatible with both object and Any. However, it is also true that Any is compatible with every type. And it's this last rule that is why you cannot remove Any from intersections—for the same reason that you cannot remove any other arbitrary type from intersections.

I quite literally didn't ignore this, I just said it's irrelevant to the other operand

An Intersection Any & T imposes two requirements, Any, and T. Any is always satisfied, this is Why we can remove it trivially from a semantic construct that means "satisfy all of these conditions" T must still be satisfied.

The best way to analyze these problems is to focus on examples

I gave clear examples of this that don't use Any to demonstrate the motivations of an intersection as given in the post. If you go to the very beginning, the motivating example is two abstract base classes that both need implementing. Clearly both must be satisfied. Any being always satisfied doesn't say anything about other non-any requirements.

@NeilGirdhar
Copy link

We're going to go around in circles if you keep repeating your intuitions. Please let's focus on the actual examples which we left off in this comment.

@mikeshardmind
Copy link

mikeshardmind commented Jul 22, 2023

We're going to go around in circles if you keep repeating your intuitions. Please let's focus on the actual examples which we left off in this comment.

Fine, I'll bite.

The example you linked doesn't contain an intersection at all and isn't a good representative of the issue I described. I'm not speaking about just "intuitions" but the literal definition in the thread of an intersection and the motivations of them.

your example that you claim has no errors fails when using settings in mypy that exist specifically to detect this kind of problem as well as at runtime. But It's irrelevant. There's no intersection here.

def function(x: Any):
    x.f()

If I instead modify it to

def function(x: Any & T):
    ...

The requirements of x are now "Is all of the types: Any, T" (see original post, current draft, etc)

While Any can be anything, there are things that are not T

The original motivating bit on this at the very beginning of this, years ago is "how do I specify I require that people have implemented both of these abstract base classes" (paraphrased) not one or the other (this would just be "just use Union") but both

@NeilGirdhar
Copy link

NeilGirdhar commented Jul 22, 2023

The example you linked doesn't contain an intersection at all and isn't a good representative of the issue I described. I'

I know that there's no intersection. But you said:

Do you accept that f is in Any's interface?

No.

How can that code pass type checking if f is not part of Any's interface?

@tylerlaprade
Copy link

@NeilGirdhar, it seems you may have misinterpreted @kmillikin's comment to be more general than it actually was. You appear to have conflated the definitions of Unions and Intersections, which is why @mikeshardmind is helping clarify the definitions for you - this is also why "sticking to examples" won't help.

@JelleZijlstra
Copy link
Member

I think this discussion is rapidly becoming unproductive and you should both take a break here.

I'll just say that as far as I can tell @mikeshardmind is right about how intersections should operate. An object is compatible with A | B if it is compatible with A or it is compatible with B; it is compatible with A & B if it is compatible with A and it is compatible with B. On the contrary, if you have an object of type A | B, you can only do something with it (e.g., accessing an attribute) if that operation is valid on A and on B (e.g., if the attribute exists on both). But if you have an object of type A & B, then you can do something with it if the operation is valid on A or on B (e.g., if the attribute exists on either of the two).

@thibaut-st
Copy link

Any is always satisfied

Doesn't that mean that Any & T is always Any? In certain language if(A && B) satify A, B won't even be checked, so in Any & T, why would we need to satisfy anything after Any?

From what I remember one of the aspect of Python is assuming that we are grown up. And if someone want to add an Any somewhere and it fuck the intersection, let it be (or ask mypy to check for no Any in the code).

Implementation wise maybe there is a specific condition that make it easier to discard Any in type intersection but it's counter intuitive.

@Jackenmen
Copy link
Contributor

Doesn't that mean that Any & T is always Any? In certain language if(A && B) satify A, B won't even be checked, so in Any & T, why would we need to satisfy anything after Any?

I think you may have confused &/&& (bitwise/logical AND operator) with |/|| (bitwise/logical OR operator)? The latter is the one that requires only one condition to be satisfied and the "equivalent" in set/type theory would be a union (expressed in Python with typing.Union or | operator) which requires only one type to be satisfied.

This issue is about adding an intersection type and the whole point of it is for the variable to have to satisfy all of the specified types at the same time as opposed to the already existing union type. Within the definition provided by Python's type theory (see PEP 483) as well as the PEP draft, the intersection is defined as "a type that is a subtype of all of its arguments" which is consistent with union's definition of being "a type that is a subtype of at least one of its arguments".

@CarliJoy
Copy link

CarliJoy commented Jul 23, 2023

Hello everybody,

I agree with @JelleZijlstra that discussion became unproductive, also simply to do the fact github issues become become very unreadable with a large amount of comments.
Also it is correct that in the current linked specification is an error. Please bear with me while we are still editing.

Nobody can follow the discussion here as not even all comments are loaded by default and they are undirected because there are no threads.

I will try to deduct a logical chain of expressions about each discussed topic here and create an Issues for each in my example repo.
I hope this will help streamlining this discussion a bit more.

⚠️ So please for the meanwhile stop discussing here for the moment, it will be too hard to follow.

For the following topics, you can discuss in the related issues, please note the instructions in each issue on how to contribute:

@mwerezak
Copy link

mwerezak commented Jul 23, 2023

I just want to point out that, practically speaking, there are scenarios where it would be very frustrating if having Any in an intersection was disallowed (causing an exception to be raised) or blew up the intersection into Any (which IMO would be destructive, i.e. loss of information and control).

Suppose I have:

def foo(x: A & B): ...

I want to make sure that whatever is passed in has both A and B's interface. That's the entire point of using the intersection there, To control what can be passed in without tripping the type checker.

Now suppose B is imported from some 3rd party library, and unbeknownst to me is actually a type alias to Any. Maybe the interfaces that work with B can take any value, but they wanted to future proof it in case that changes. Or it's temporary or the library's incomplete or they were lazy or whatever.

Now my type annotation has been completely obliterated. Even if B could be anything I still want the invariant that whatever goes in there conforms to A. If an intersection with Any decays to Any that doesn't happen and furthermore how will I express what I want? Or if it's disallowed now an exception is being raised and I have to come up with some weird workaround instead of just putting what I actually want.

Am I expecting the wrong thing here? I absolutely do not want Any to mean "turn off the type checker!" That might be the effect of using it in a lot of contexts but that's not what it means to me. I may be incorrect, but I've always thought of it as meaning "potentially any type", like the universal set. The intersection of something with the universal set with something generally isn't the universal set. Even if my understanding of what Any means is backwards I bet you that's how a lot of people will think about it.

@CarliJoy
Copy link

CarliJoy commented Jul 24, 2023

Current State of Intersections in Python

There are a bunch of people working on a PEP and a the specification for Intersection now.
We can summarize there is a lot of interest in getting this done finally but poses quite some challenges.

This issues here is very old and very long and handles a lot of different things that have already been moved to different issues i.e. Introduce a Not type, the handling of Any and much more.
It is very hard to keep track of things here, you need to click on load message at least three time to get message not even a week old.

We keep the current WIP specification in an extra repo.

Could we maybe lock the discussion on this issue?

Further discussion should go into the correct sub issues related to the specification.

The general state of the PEP is tracked in this issues

@andersk

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests