Let’s consider this motivating example (Edit: Originally when writing this post I had forgotten Protocol in CanNeg’s definition, so I’ve fixed it.):
from typing import Generic, Protocol, TypeVar, final, reveal_type
type Arg = int | str
class CanNeg[Out](Protocol):
def __neg__(self, /) -> Out: ...
_T_co = TypeVar("_T_co", bound=Arg, covariant=True)
@final
class Foo(Generic[_T_co]):
def __init__(self, value: _T_co, /) -> None: ...
@property
def value(self, /) -> _T_co: ...
def __neg__[U: Arg](self: Foo[CanNeg[U]], /) -> Foo[U]: # error
return Foo(-self.value) # pyright does not understand that self.value: CanNeg[U]
x: Foo[int] = Foo(42)
reveal_type(-x.value) # int
reveal_type(-x) # Foo[int]
The definition of Foo.__neg__ here is wrong, because Foo[CanNeg[U]] is ill-formed: the statement "for all types X, X <: CanNeg[U] implies X <: Arg” is false.
If we had intersections between a concrete type and a protocol, one would write Foo[CanNeg[U] & Arg].
Note: I know one could’ve just used
self: Foo[int], but that’s not the point. I have a codebase where I need to annotate something whereArgis a way more complex union of very different types (from external libraries), each one with a complex__neg__definition that does not always returnSelf. I’d like to annotateFoo.__neg__without needing to worry about those return types and without needing to repeat a plethora of overloads over and over again.Relaxing the
Argbound is probably the best option as of now, but it’s suboptimal and would allow writing invalid types, such asFoo[None](in this example).
I think it would be interesting to consider giving the same “intersection” meaning to the pattern in this example. More formally:
Let A[+T: B] be a type, generic over a covariant type variable T with bound B, and let P be a protocol.
Then, the expression A[P] represents the union of all types A[X] with X <: P and X <: B.
Therefore:
- if
Xis a type,A[X] <: A[P]iffX <: BandX <: P; - if
Yis a type,Y <: A[P]iff there exists anX <: Bsuch thatY <: A[X]andX <: P.
Then, Foo[CanNeg[U]] in the example would essentially mean Foo[CanNeg[U] & Arg] and the code would type check as intended. Importantly, this would work without needing to introduce any syntax change, and would be backwards-compatible - it just gives meaning to currently invalid annotations.
It’s worth noting that no bound is equivalent to a bound of object (per the spec, see object’s usage as the top type and TypeVar upper bound definition): in this case (B = object), I believe the semantics I’ve just described are equivalent to the status quo.
Regarding forwards-compatibility with user-expressible intersections, I think this should not be much of a problem, although intersections would include this behaviour making this proposal obsolete. I still think it’s a nice shorthand, intended for long classes with a lot of functions where one might not want to repeat the & Arg bound over and over again. I know, explicit is better than implicit, but too explicit might sometimes harm readibility (and maintainability), especially in big, complex codebases.
I’d be curious to get a sense of how difficult would it be to implement this behaviour in type checkers. In case the implementation happens to be too challenging, I guess it’s possible to restrict this behaviour to the following special cases:
Ais a concrete type (meaning, not aProtocol),Bis a concrete type (although I don’t expectBbeing aProtocolto be much of a problem).
Edit: It’s worth noting that pyright does seem to partially understand the proposed pattern correctly, see the playground link above.
Here, I’ve always taken A as covariant in T because if T was invariant, the type A[P] would imply X <: P <: X in the above, which I don’t think is very useful if X is intended to be a concrete type (for example, if B is a concrete type).
Disclaimer: This post is just intended to get a sense of whether or not it might make sense to introduce something like this in the type system.
I’ve searched this forum for similar proposals and found none, but if I’ve missed something, please let me know.
I also don’t think I’d have the capacity (nor the experience), as of now, to write a PEP or a similarly formal proposal.