🐍 Python - Type Annotations
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
- Python Tricks The Book, Dan Baderƒ
- Fluent Python, Luciano Ramalho
- Mypy - Type hints cheat sheet (Python 3)