Skip to content

Commit bc6276d

Browse files
author
Guido van Rossum
committed
Add type variables with upper bound. (Fixes issue #59.)
1 parent d2d21f1 commit bc6276d

3 files changed

Lines changed: 74 additions & 8 deletions

File tree

‎pep-0484.txt‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,44 @@ or properties, and generic classes can also have ABCs as base
457457
classes without a metaclass conflict.
458458

459459

460+
Type variables with an upper bound
461+
----------------------------------
462+
463+
A type variable may specify an upper bound using ``bound=<type>``.
464+
This means that an actual type substituted (explicitly or implictly)
465+
for the type variable must be a subclass of the boundary type. A
466+
common example is the definition of a Comparable type that works well
467+
enough to catch the most common errors::
468+
469+
class Comparable(metaclass=ABCMeta):
470+
@abstractmethod
471+
def __lt__(self, other: Any) -> bool: ...
472+
... # __gt__ etc. as well
473+
474+
CT = TypeVar('CT', bound=Comparable)
475+
476+
def min(x: CT, y: CT) -> CT:
477+
if x < y:
478+
return x
479+
else:
480+
return y
481+
482+
min(1, 2) # ok, return type int
483+
min('x', 'y') # ok, return type str
484+
485+
(Note that this is not ideal -- for example ``min('x', 1)`` is invalid
486+
at runtime but a type checker would simply infer the return type
487+
``Comparable``. Unfortunately, addressing this would require
488+
introducing a much more powerful and also much more complicated
489+
concept, F-bounded polymorphism. We may revisit this in the future.)
490+
491+
An upper bound cannot be combined with type constraints (as in used
492+
``AnyStr``, see the example earlier); type constraints cause the
493+
inferred type to be _exactly_ one of the constraint types, while an
494+
upper bound just requires that the actual type is a subclass of the
495+
boundary type.
496+
497+
460498
The numeric tower
461499
-----------------
462500

‎prototyping/test_typing.py‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ def test_basic_constrained(self):
147147
# A is a subclass of itself.
148148
assert issubclass(A, A)
149149

150+
def test_constrained_error(self):
151+
with self.assertRaises(TypeError):
152+
X = TypeVar('X', int)
153+
150154
def test_union_unique(self):
151155
X = TypeVar('X')
152156
Y = TypeVar('Y')
@@ -181,7 +185,8 @@ def test_subclass_as_unions(self):
181185
# None of these are true -- each type var is its own world.
182186
self.assertFalse(issubclass(TypeVar('T', int, str),
183187
TypeVar('T', int, str)))
184-
self.assertFalse(issubclass(TypeVar('T', int), TypeVar('T', int, str)))
188+
self.assertFalse(issubclass(TypeVar('T', int, float),
189+
TypeVar('T', int, float, str)))
185190
self.assertFalse(issubclass(TypeVar('T', int, str),
186191
TypeVar('T', str, int)))
187192
A = TypeVar('A', int, str)
@@ -203,6 +208,18 @@ def test_cannot_instantiate_vars(self):
203208
with self.assertRaises(TypeError):
204209
TypeVar('A')()
205210

211+
def test_bound(self):
212+
X = TypeVar('X', bound=Employee)
213+
assert issubclass(Employee, X)
214+
assert issubclass(Manager, X)
215+
assert not issubclass(int, X)
216+
217+
def test_bound_errors(self):
218+
with self.assertRaises(TypeError):
219+
TypeVar('X', bound=42)
220+
with self.assertRaises(TypeError):
221+
TypeVar('X', str, float, bound=Employee)
222+
206223

207224
class UnionTests(TestCase):
208225

@@ -345,7 +362,7 @@ def test_var_union_subclass(self):
345362
self.assertTrue(issubclass(KT, Union[KT, VT]))
346363

347364
def test_var_union(self):
348-
TU = TypeVar('TU', Union[int, float])
365+
TU = TypeVar('TU', Union[int, float], None)
349366
assert issubclass(int, TU)
350367
assert issubclass(float, TU)
351368
with self.assertRaises(TypeError):

‎prototyping/typing.py‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -374,14 +374,23 @@ def longest(x: A, y: A) -> A:
374374
A.__constraints__ == (str, bytes)
375375
"""
376376

377-
def __new__(cls, name, *constraints, covariant=False, contravariant=False):
377+
def __new__(cls, name, *constraints, bound=None,
378+
covariant=False, contravariant=False):
378379
self = super().__new__(cls, name, (Final,), {}, _root=True)
379380
if covariant and contravariant:
380-
raise ValueError("bivariant type variables are not supported")
381-
msg = "TypeVar(name, constraint, ...): constraints must be types."
381+
raise ValueError("Bivariant type variables are not supported.")
382382
self.__covariant__ = bool(covariant)
383383
self.__contravariant__ = bool(contravariant)
384+
if constraints and bound is not None:
385+
raise TypeError("Constraints cannot be combined with bound=<type>.")
386+
if constraints and len(constraints) == 1:
387+
raise TypeError("A single constraint is not allowed")
388+
msg = "TypeVar(name, constraint, ...): constraints must be types."
384389
self.__constraints__ = tuple(_type_check(t, msg) for t in constraints)
390+
if bound:
391+
self.__bound__ = _type_check(bound, "Bound must be a type.")
392+
else:
393+
self.__bound__ = None
385394
return self
386395

387396
def _has_type_var(self):
@@ -401,13 +410,15 @@ def __instancecheck__(self, instance):
401410

402411
def __subclasscheck__(self, cls):
403412
# TODO: Make this raise TypeError too?
404-
if not self.__constraints__:
405-
return True
406413
if cls is self:
407414
return True
408415
if cls is Any:
409416
return True
410-
return any(issubclass(cls, c) for c in self.__constraints__)
417+
if self.__bound__ is not None:
418+
return issubclass(cls, self.__bound__)
419+
if self.__constraints__:
420+
return any(issubclass(cls, c) for c in self.__constraints__)
421+
return True
411422

412423

413424
# Some unconstrained type variables. These are used by the container types.

0 commit comments

Comments
 (0)