ruk·si

🐍 Python
Type Annotations

Updated at 2018-06-09 15:47

Don't annotate too much. You can easily go overboard with type hinting, making the code unreadable.

You can define types for your entities.

a: int = 1
b: float = 1.0
c: bool = True
d: str = "test"
e: str = u"test"
f: bytes = b"test"
from typing import List, Set, Dict, Tuple

g: List[int] = [1]
h: Set[int] = {6, 7}
i: Dict[str, float] = {'field': 2.0}
j: Tuple[int, str, float] = (3, "yes", 7.5)
from typing import Callable

def stringify(num: int) -> str:
    return str(num)

def f(num1: int, my_float: float = 3.5) -> float:
    return num1 + my_float

m: Callable[[int, float], float] = f
# Callable[..., float] would mean that "no idea about the parameters"
from typing import Iterable

def f(n: int) -> Iterable[int]:
    i = 0
    while i < n:
        yield i
        i += 1

Class variables typed with ClassVar.

from typing import ClassVar, List

class Person:
    name: str
    age: int = 18

    people: ClassVar[List['Person']] = []

    def __init__(self) -> None:
        Person.people.append(self)

john = Person()
alice = Person()
bob = Person()
assert Person.people == [john, alice, bob]

Generics can be typed with TypeVar and classes (not the instances) with Type.

from typing import Type, TypeVar

class Person:

    def __init__(self, name) -> None:
        self.name: str = name

    def greet(self) -> str:
        return f'{self.name}: Hello!'

class User(Person):
    pass

class Admin(User):
    pass

P = TypeVar('P', bound=Person)

def new_user(user_class: Type[P], **kwargs) -> P:
    user = user_class(**kwargs)
    return user

john = new_user(User, name='John')
assert john.name == 'John'
assert john.greet() == 'John: Hello!'
assert type(john) == User

bob = new_user(Admin, name='Bob')
assert bob.name == 'Bob'
assert bob.greet() == 'Bob: Hello!'
assert type(bob) == Admin

Type aliases should be in PascalCase.

from typing import TypeVar, Tuple, Union, Callable, Iterable

S = TypeVar('S')
TInt = Tuple[int, S]
UInt = Union[S, int]
CBack = Callable[..., S]

T = TypeVar('T', int, float, complex)
Vec = Iterable[Tuple[T, T]]

Use Optional if value can be None.

from typing import Optional

k: Optional[str] = some_function()
if k is not None:
    print(k)

Use Union if value can be of multiple types.

from typing import List, Union

l: List[Union[int, str]] = [3, 5, 'test', 'fun']

Use Any if value is too dynamic to type. Any works with all types and it is default type when nothing else has been defined.

from typing import Any, List

m: List[Any] = [1, 2.0, 'test', b'fun', None]

Use cast to change type of a variable.

from typing import List, cast

a = [4]
b = cast(List[int], a)
c = cast(List[str], a)

b.append(123)
b.append('word')  # type warning!

c.append('word')
c.append(123)  # type warning!

Use NoReturn if a function will never return.

from typing import NoReturn

def stop() -> NoReturn:
    raise Exception('no way')

Use NewType to give meaning to built-in types.

from typing import NewType

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    return str(user_id)

UserId('user')           # fail
name_by_id(42)           # fail
name_by_id(UserId(42))   # ok
num: int = UserId(5) + 1 # ok

Typing *args and **kwargs type all respective arguments. It is common to just not type these or use Any.

def call(*args: str, **kwargs: int) -> str:
    print(args)  # all should be strings
    print(kwargs)  # all should be integers
    return 'done'

# valid:
call()
call('hello')
call(index=1)
call('bye', stuff=2)

# type errors:
call('hello', 1)
call(msg='This is an error.')

The only thing Python does with type annotations is store them in __annotations__ attribute of the function. It is up to external tooling and libraries to use the annotations.

def greet(first_name: str, last_name: str = 'Doe', *args, **kwargs) -> str:
    """ Greet the given person. """
    return f'Hello, {first_name} {last_name}!'

assert greet.__annotations__ == {
    'first_name': str,
    'last_name': str,
    'return': str,
}

inspect module provides tools to read the annotations.

from inspect import signature, _ParameterKind, _empty

def greet(first_name: str, last_name: str = 'Doe', *args, **kwargs) -> str:
    """ Greet the given person. """
    return f'Hello, {first_name} {last_name}!'

assert greet('Ruksi') == 'Hello, Ruksi Doe!'

sig = signature(greet)
assert list(sig.parameters) == ['first_name', 'last_name', 'args', 'kwargs']

assert sig.parameters['first_name'].kind is _ParameterKind.POSITIONAL_OR_KEYWORD
assert sig.parameters['last_name'].kind is _ParameterKind.POSITIONAL_OR_KEYWORD
assert sig.parameters['args'].kind is _ParameterKind.VAR_POSITIONAL
assert sig.parameters['kwargs'].kind is _ParameterKind.VAR_KEYWORD

assert sig.parameters['first_name'].annotation is str
assert sig.parameters['last_name'].annotation is str
assert sig.parameters['args'].annotation is _empty
assert sig.parameters['kwargs'].annotation is _empty

assert sig.parameters['first_name'].default is _empty
assert sig.parameters['last_name'].default == 'Doe'
assert sig.parameters['args'].default is _empty
assert sig.parameters['kwargs'].default is _empty

assert sig.return_annotation is str

Source