2014-0484-Type-Hints
https://peps.python.org/pep-0484
This PEP introduces a provisional module to provide these standard definitions and tools, along with some conventions for situations where annotations are not available
The proposal is strongly inspired by mypy
Type Definition Syntax
Acceptable type hints
Type hints may be built-in classes (including those defined in standard library or third-party extension modules), abstract base classes, types available in the types module, and user-defined classes (including those defined in the standard library or 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 stub file
In addition to the above, the following special constructs defined below may be used: None
, Any
, Union
, Tuple
, Callable
, all ABCs
and stand-ins for concrete classes exported from typing
(e.g. Sequence
and Dict
), type variables, and type aliases
Using None
When used in a type hint, the expression None is considered equivalent to type(None)
Type aliases
Url = str
def retry(url: Url, retry_count: int) -> None: ...
from typing import TypeVar, Iterable, Tuple
T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]
def inproduct(v: Vector[T]) -> T:
return sum(x*y for x, y in v)
def dilate(v: Vector[T], scale: T) -> Vector[T]:
return ((x * scale, y * scale) for x, y in v)
vec = [] # type: Vector[float]
Callable
from typing import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
# Body
It is possible to declare the return type of a callable without specifying the call signature by substituting a literal ellipsis (three dots) for the list of arguments
def partial(func: Callable[..., str], *args) -> Callable[..., str]:
# Body
Generics
from typing import Mapping, Set
def notify_by_email(employees: Set[Employee], overrides: Mapping[str, str]) -> None: ...
from typing import Sequence, TypeVar
T = TypeVar('T') # Declare type variable
def first(l: Sequence[T]) -> T: # Generic function
return l[0]
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
User-defined generic types
You can include a Generic
base class to define a user-defined class as generic
from typing import TypeVar, Generic
from logging import Logger
T = TypeVar('T')
class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
self.value = value
def set(self, new: T) -> None:
self.log('Set ' + repr(self.value))
self.value = new
def get(self) -> T:
self.log('Get ' + repr(self.value))
return self.value
def log(self, message: str) -> None:
self.logger.info('{}: {}'.format(self.name, message))
The Generic
base class uses a metaclass that defines __getitem__
so that LoggedVar[t]
is valid as a type
from typing import Iterable
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
Subclassing a generic class without specifying type parameters assumes Any
for each position. In the following example, MyIterable is not generic but implicitly inherits from Iterable[Any]
from typing import Iterable
class MyIterable(Iterable): # Same as Iterable[Any]
...
Scoping rules for type variables
- A type variable used in a method that does not match any of the variables that parameterize the class makes this method a generic function in that variable
T = TypeVar('T')
S = TypeVar('S')
class Foo(Generic[T]):
def method(self, x: T, y: S) -> S:
...
x = Foo() # type: Foo[int]
y = x.method(0, "abc") # inferred type of y is str
- Unbound type variables should not appear in the bodies of generic functions, or in the class bodies apart from method definitions
T = TypeVar('T')
S = TypeVar('S')
def a_fun(x: T) -> None:
# this is OK
y = [] # type: List[T]
# but below is an error!
y = [] # type: List[S]
class Bar(Generic[T]):
# this is also an error
an_attr = [] # type: List[S]
def do_something(x: S) -> S: # this is OK though
...
Instantiating generic classes and type erasure
from typing import TypeVar, Generic
T = TypeVar('T')
class Node(Generic[T]):
x = None # type: T # Instance attribute (see below)
def __init__(self, label: T = None) -> None:
...
x = Node('') # Inferred type is Node[str]
y = Node(0) # Inferred type is Node[int]
z = Node() # Inferred type is Node[Any]
To create Node instances you call Node()
just as for a regular class. At runtime the type (class) of the instance will be Node
. But what type does it have to the type checker? The answer depends on how much information is available in the call. If the constructor (__init__
or __new__
) uses T
in its signature, and a corresponding argument value is passed, the type of the corresponding argument(s) is substituted. Otherwise, Any
is assumed
In case the inferred type uses [Any]
but the intended type is more specific, you can use a type comment (see below) to force the type of the variable
a = Node() # type: Node[int]
b = Node() # type: Node[str]
Alternatively, you can instantiate a specific concrete type
p = Node[int]()
q = Node[str]()
Node[int]
and Node[str]
are distinguishable class objects, but the runtime class of the objects created by instantiating them doesn’t record the distinction. This behavior is called “type erasure”
It is not recommended to use the subscripted class (e.g. Node[int]
) directly in an expression – using a type alias (e.g. IntNode = Node[int]
) instead is preferred. (First, creating the subscripted class, e.g. Node[int]
, has a runtime cost. Second, using a type alias is more readable.)
Abstract generic types
The metaclass used by Generic
is a subclass of abc.ABCMeta
. A generic class can be an ABC by including abstract methods or properties, and generic classes can also have ABCs as base classes without a metaclass conflict
Type variables with an upper bound
A type variable may specify an upper bound using bound=<type>
(note: <type>
itself cannot be parameterized by type variables). This means that an actual type substituted (explicitly or implicitly) for the type variable must be a subtype of the boundary type
from typing import TypeVar, Sized
ST = TypeVar('ST', bound=Sized)
def longer(x: ST, y: ST) -> ST:
if len(x) > len(y):
return x
else:
return y
longer([1], [1, 2]) # ok, return type List[int]
longer({1}, {1, 2}) # ok, return type Set[int]
longer([1], {1, 2}) # ok, return type Collection[int]
Covariance and contravariance
The read-only collection classes in typing are all declared covariant in their type variable (e.g. Mapping
and Sequence
). The mutable collection classes (e.g. MutableMapping
and MutableSequence
) are declared invariant. The one example of a contravariant type is the Generator
type, which is contravariant in the send()
argument type
Variance is only applicable to generic types; generic functions do not have this property
Union types
from typing import Union
def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
if isinstance(e, Employee):
e = [e]
...
def handle_employee(e: Union[Employee, None]) -> None: ...
from typing import Optional
def handle_employee(e: Optional[Employee]) -> None: ...
Support for singleton types in unions
class Reason(Enum):
timeout = 1
error = 2
def process(response: Union[str, Reason] = '') -> str:
if response is Reason.timeout:
return 'TIMEOUT'
elif response is Reason.error:
return 'ERROR'
else:
# response can be only str, all other possible values exhausted
return 'PROCESSED: ' + response
The NoReturn type
The typing
module provides a special type NoReturn
to annotate functions that never return normally
from typing import NoReturn
def stop() -> NoReturn:
raise RuntimeError('no way')
The type of class objects
U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
user = user_class()
...
def new_non_team_user(user_class: Type[Union[BasicUser, ProUser]]):
user = new_user(user_class)
...
Annotating instance and class methods
T = TypeVar('T', bound='Copyable')
class Copyable:
def copy(self: T) -> T:
# return a copy of self
class C(Copyable): ...
c = C()
c2 = c.copy() # type here should be C
T = TypeVar('T', bound='C')
class C:
@classmethod
def factory(cls: Type[T]) -> T:
# make a new instance of cls
class D(C): ...
d = D.factory() # type here should be D
Annotating generator functions and coroutines
Generator[yield_type, send_type, return_type]
def echo_round() -> Generator[int, float, str]:
res = yield
while res:
res = yield round(res)
return 'OK'