diff --git a/pep-0484.txt b/pep-0484.txt index 2c3b641f8..c728245c2 100644 --- a/pep-0484.txt +++ b/pep-0484.txt @@ -9,7 +9,7 @@ Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 29-Sep-2014 -Post-History: 16-Jan-2015,20-Mar-2015,17-Apr-2015,20-May-2015 +Post-History: 16-Jan-2015,20-Mar-2015,17-Apr-2015,20-May-2015,22-May-2015 Resolution: @@ -109,8 +109,12 @@ annotations for all arguments and the return type. For a checked function, the default annotation for arguments and for the return type is ``Any``. An exception is that the first argument of instance and class methods does not need to be annotated; it is assumed to have the -type of the containing class for instance methods, and ``type`` for -class methods. +type of the containing class for instance methods, and a type object +type corresponding to the containing class object for class methods. +For example, in class ``A`` the first argument of an instance method +has the implicit type ``A``. In a class method, the precise type of +the first argument cannot be represented using the available type +notation. (Note that the return type of ``__init__`` ought to be annotated with ``-> None``. The reason for this is subtle. If ``__init__`` assumed @@ -158,7 +162,7 @@ third-party modules). While annotations are normally the best format for type hints, there are times when it is more appropriate to represent them -by a special comment, or in a separately distributed interface +by a special comment, or in a separately distributed stub file. (See below for examples.) Annotations must be valid expressions that evaluate without raising @@ -284,6 +288,11 @@ Generics can be parametrized by using a new factory available in In this case the contract is that the returned value is consistent with the elements held by the collection. +A ``TypeVar()`` expression must always directly be assigned to a +variable (it should not be used as part of a larger expression). The +argument to ``TypeVar()`` must be a string equal to the variable name +to which it is assigned. Type variables must not be redefined. + ``TypeVar`` supports constraining parametric types to a fixed set of possible types. For example, we can define a type variable that ranges over just ``str`` and ``bytes``. By default, a type variable ranges @@ -406,25 +415,57 @@ is not generic but implicitly inherits from ``Iterable[Any]``: class MyIterable(Iterable): # Same as Iterable[Any] ... +Generic metaclasses are not supported. + Instantiating generic classes and type erasure ---------------------------------------------- Generic types like ``List`` or ``Sequence`` cannot be instantiated. However, user-defined classes derived from them can be instantiated. -Given a generic class ``Node[T]`` there are three forms of -instantiation: +Suppose we write a ``Node`` class inheriting from ``Generic[T]``:: -* ``x = Node()`` -- the type of x is ``Node[Any]``. + from typing import TypeVar, Generic -* ``x = Node[T]()`` -- the type of x is ``Node[T]``. + T = TypeVar('T') -* ``x = Node[int]()`` -- the type of x is ``Node[int]``. + class Node(Generic[T]): + ... -At runtime the type is not preserved, and the observable type of x is -just ``Node``. This is type erasure and common practice in languages -with generics (e.g. Java, Typescript). +Now there are two ways we can instantiate this class; the type +inferred by a type checker may be different depending on the form we +use. The first way is to give the value of the type parameter +explicitly -- this overrides whatever type inference the type +checker would otherwise perform: + x = Node[T]() # The type inferred for x is Node[T]. + + y = Node[int]() # The type inferred for y is Node[int]. + +If no explicit types are given, the type checker is given some +freedom. Consider this code: + + x = Node() + +The inferred type could be ``Node[Any]``, as there isn't enough +context to infer a more precise type. Alternatively, a type checker +may reject the line and require an explicit annotation, like this: + + x = Node() # type: Node[int] # Inferred type is Node[int]. + +A type checker with more powerful type inference could look at how +``x`` is used elsewhere in the file and try to infer a more precise +type such as ``Node[int]`` even without an explicit type annotation. +However, it is probably impossible to make such type inference work +well in all cases, since Python programs can be very dynamic. + +This PEP doesn't specify the details of how type inference should +work. We allow different tools to experiment with various approaches. +We may give more explicit rules in future revisions. + +At runtime the type is not preserved, and the class of ``x`` is just +``Node`` in all cases. This behavior is called "type erasure"; it is +common practice in languages with generics (e.g. Java, TypeScript). Arbitrary generic types as base classes @@ -565,28 +606,30 @@ checking is acceptable, a type variable can be declared using is desirable, pass ``contravariant=True``. At most one of these may be passed. -A typical example involves defining an immutable container class:: +A typical example involves defining an immutable (or read-only) +container class:: - from typing import TypeVar + from typing import TypeVar, Generic, Iterable, Iterator T = TypeVar('T', covariant=True) class ImmutableList(Generic[T]): - def append(self, T): ... + def __init__(self, items: Iterable[T]) -> None: ... + def __iter__(self) -> Iterator[T]: ... ... class Employee: ... class Manager(Employee): ... - def dump_employees(emps: ImmutableList[Employee]) -> None: ... - - mgrs = ... # type: ImmutableList[Mananger] - mgrs.append(Manager()) + def dump_employees(emps: ImmutableList[Employee]) -> None: + for emp in emps: + ... + mgrs = ImmutableList([Manager()]) # type: ImmutableList[Manager] dump_employees(mgrs) # OK -The immutable collection classes in ``typing`` are all defined using a +The read-only collection classes in ``typing`` are all defined using a covariant type variable (e.g. ``Mapping`` and ``Sequence``). The mutable collection classes (e.g. ``MutableMapping`` and ``MutableSequence``) are defined using regular invariant type @@ -676,7 +719,7 @@ To address this, we write:: self.right = right The string literal should contain a valid Python expression (i.e., -``compile(lit, '', 'expr')`` should be a valid code object) and it +``compile(lit, '', 'eval')`` should be a valid code object) and it should evaluate without errors once the module has been fully loaded. The local and global namespace in which it is evaluated should be the same namespaces in which default arguments to the same function would @@ -711,8 +754,8 @@ possible to import all the needed models directly:: def bar(self, a: A): ... # File main.py - from a import A - from b import B + from models.a import A + from models.b import B Assuming main is imported first, this will fail with an ImportError at the line ``from models.a import A`` in models/b.py, which is being @@ -731,8 +774,8 @@ _module_._class_ name:: def bar(self, a: 'a.A'): ... # File main.py - from a import A - from b import B + from models.a import A + from models.b import B Union types @@ -790,31 +833,26 @@ allow all operations on it, and a value of type ``Any`` can be assigned to a variable (or used as a return value) of a more constrained type. -Predefined constants --------------------- +Version and platform checking +----------------------------- -Some predefined Boolean constants are defined in the ``typing`` -module to enable platform-specific type definitions and such:: +Type checkers are expected to understand simple version and platform +checks, e.g.:: - from typing import PY2, PY3, WINDOWS, POSIX + import sys - if PY2: - text = unicode + if sys.version_info[0] >= 3: + # Python 3 specific definitions else: - text = str + # Python 2 specific definitions - def f() -> text: ... - - if WINDOWS: - loop = ProactorEventLoop + if sys.platform == 'win32': + # Windows specific definitions else: - loop = UnixSelectorEventLoop + # Posix specific definitions -It is up to the type checker implementation to define their values, as -long as ``PY2 == not PY3`` and ``WINDOWS == not POSIX``. When the -program is being executed these always reflect the current platform, -and this is also the suggested default when the program is being -type-checked. +Don't expect a checker to understand obfuscations like +``"".join(reversed(sys.platform)) == "xunil"``. Default argument values @@ -942,7 +980,7 @@ than a type checker may be able to infer. For example:: # We only get here if there's at least one string in a return cast(str, a[index]) -Some type checkers may not be able to infers that the type of +Some type checkers may not be able to infer that the type of ``a[index]`` is ``str`` and only infer ``object`` or ``Any``", but we know that (if the code gets to that point) it must be a string. The ``cast(t, x)`` call tells the type checker that we are confident that @@ -1010,20 +1048,21 @@ follows:: from typing import overload class bytes: - ... - @overload - def __getitem__(self, i: int) -> int: ... - @overload - def __getitem__(self, s: slice) -> bytes: ... + ... + @overload + def __getitem__(self, i: int) -> int: ... + @overload + def __getitem__(self, s: slice) -> bytes: ... This description is more precise than would be possible using unions (which cannot express the relationship between the argument and return types):: from typing import Union + class bytes: - ... - def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]: ... + ... + def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]: ... Another example where ``@overload`` comes in handy is the type of the builtin ``map()`` function, which takes a different number of @@ -1064,6 +1103,35 @@ to be constrained by the overloading syntax defined for type hints in stub files. In the meantime, using the ``@overload`` decorator or calling ``overload()`` directly raises ``RuntimeError``. +A constrained ``TypeVar`` type can often be used instead of using the +``@overload`` decorator. For example, the definitions of ``concat1`` +and ``concat2`` in this stub file are equivalent: + + from typing import TypeVar + + AnyStr = TypeVar('AnyStr', str, bytes) + + def concat1(x: AnyStr, y: AnyStr) -> AnyStr: ... + + @overload + def concat2(x: str, y: str) -> str: ... + @overload + def concat2(x: bytes, y: bytes) -> bytes: ... + +Some functions, such as ``map`` or ``bytes.__getitem__`` above, can't +be represented precisely using type variables. However, unlike +``@overload``, type variables can also be used outside stub files. We +recommend that ``@overload`` is only used in cases where a type +variable is not sufficient, due to its special stub-only status. + +Another important difference between type variables such as ``AnyStr`` +and using ``@overload`` is that the prior can also be used to define +constraints for generic class type parameters. For example, the type +parameter of the generic class ``typing.IO`` is constrained (only +``IO[str]``, ``IO[bytes]`` and ``IO[Any]`` are valid): + + class IO(Generic[AnyStr]): ... + Storing and distributing stub files ----------------------------------- @@ -1162,6 +1230,8 @@ Fundamental building blocks: * TypeVar, used as ``X = TypeVar('X', Type1, Type2, Type3)`` or simply ``Y = TypeVar('Y')`` (see above for more details) +* Generic, used to create user-defined generic classes + Generic variants of builtin collections: * Dict, used as ``Dict[key_type, value_type]`` @@ -1242,18 +1312,10 @@ A few one-off types are defined that test for single special methods * SupportsBytes, to test for ``__bytes__`` -Constants for platform-specific type hinting: - -* PY2 - -* PY3, equivalent to ``not PY2`` - -* WINDOWS - -* POSIX, equivalent to ``not WINDOWS`` - Convenience definitions: +* Optional, defined by ``Optional[t] == Union[t, type(None)]`` + * AnyStr, defined as ``TypeVar('AnyStr', str, bytes)`` * NamedTuple, used as