python / typing

Python static typing home. Hosts the documentation and a user help forum.
https://typing.readthedocs.io/
Other
1.59k stars 233 forks source link

Lower bound for TypeVars #674

Open poscat0x04 opened 5 years ago

poscat0x04 commented 5 years ago

Lower bounds are useful when implementing type safe methods of (classes that are covariant with some type) that take argument of some other type. For example, the method updated of scala's Seq

reference: #59

gvanrossum commented 5 years ago

Can you elaborate with an example? I'm not going to try and understand the Scala docs, and there's too much discussion in #59 to know what specifically you meant.

poscat0x04 commented 5 years ago
from typing import *

T = TypeVar("T", covariant=True)
V = TypeVar("V")

class List(Generic[T]):
    def updated(self, v: V) -> List[V]:  # V should be a super type of T
        """
        Creates a new list with the first element updated
        """
        x, *xs = self
        return [v] + xs

The argument type cannot be T since it would then occur in the contravariant position.

gvanrossum commented 5 years ago

(A little confusing since List is a PEP 484 standard type that's invariant, but I guess Sequence would work just as well, and I guess you come from a different culture.)

OK, so assuming int is a subtype of float (mypy treats it that way), If we had x: List[int], then x.updated(3.14) would be a List[float], while if we had y: List[float] then y.updated(42) should not be treated as a List[int] because it may still contain floats.

How common do you think this use case would be in real-world Python code? (I'm not interested in alternative realities where Python is a functional language. :-)

ilevkivskyi commented 5 years ago

This feature appears from time to time in various contexts, see e.g. https://github.com/python/mypy/issues/3737. So I can see the value in it, but I don't think it is a priority ATM.

JukkaL commented 5 years ago

Yeah, this would be nice every once in a while. This might help with some list methods, such as copy(), but only together with F-bounded quantification. I don't understand the updated example above. Maybe a more complete example with an implementation would make things clearer.

I agree that this is pretty low priority. We'd probably need to change typing.TypeVar to allow specifying a lower bound.

erp12 commented 3 years ago

I just hit this issue while building some custom data structure types on top of the popular pyrsistent library. I was creating custom immutable list and set types that are covariant in the element type. Without lower type bounds, I could not logically implement (at least) the following methods:

To provide a better example (not using the invariant List type) let's consider the following example using the built-in Mapping which is covariant in the value type.

from typing import *

K = TypeVar("K")
V = TypeVar("V", covariant=True)
B = TypeVar("B")

class ImmutableMap(Mapping[K, V]):

    def updated(self, key: K, value: B) -> "ImmutableMap[K, ?]":
        """ Returns a new `ImmutableMap` where the `key` is associated with a new `value`."""
        ...

The question is: what is the return type of updated? There are only a few possible scenarios:

  1. B is a sub-type of V and thus the return type would be ImmutableMap[K, V].
  2. B is a super-type of V and thus the return type would be ImmutableMap[K, B].
  3. B is some other type with no relationship to V and thus the return the must fall back on ImmutableMap[K, Any].

Since we are discussing typed collections here, scenario 3 is very rare and probably irrelevant.

Scenario 1 and 2 are both logical and (in my experience) fairly common. If we could declare B = TypeVar("B", lower_bound=A) then it would be possible to support scenarios 1 and 2 in a single method signature without losing any type information.

Without lower bounds, we can only support scenario 1 by annotating value: V which is misaligned with the method's logical semantics.

I understand this might stay low-priority. I just wanted to hopefully clear up some of the motivation and document a specific use case that hit this limitation.

Galbar commented 3 years ago

I'm having a similar problem. I've written a dependency injector for python (I know the code is horrible) and I'm adding type hints to its code so that it passes mypy checks. I have a method with the signature like this:

def bind(self, types: Tuple[Type[T], ...], instance: S, name: Optional[str] = None, singleton: bool = False) -> None:

The idea being that types is a tuple of types that instance can be represented as (all(isinstance(instance, t) for t in types)).

Is there any way this relationship between T and S can be defined?