Comments
- If your code needs to support < Python3.5
- Standard python syntax checker wont complain if you get something wrong
Example
from typing import List
def hello(): # type: () -> None
print 'hello'
class Example:
def method(self, lst, opt=0, *args, **kwargs):
# type: (List[str], int, *str, **bool) -> int
"""Docstring comes after type comment.
dont give a type to self
note * and **
"""
etc
@classmethod
def cls_method(cls,lst)
# type: (List[str]) -> int
"""dont give a type to cls"""
i = 1 # type: int
ex = Example() # type: Example
Example
from typing import List
class Example:
def method(self,
lst, # type: List[str]
opt=0, # type: int
*args, # type: str
**kwargs # type: bool
):
# type: (...) -> int
"""
note LACK of * and **
"""
etc
Types can be...
- basic Python types (`int`, `str`, etc)
- Imported from the `typing` module
- None
- classes
- subscriptable (`List[str]`)
Excercise
Add type comments to caesar.py
mypy --py2 program.py
# or
mypy program.py
Answer
import string
from typing import List
characters = string.ascii_letters + string.digits
count = len(characters)
def encode(s, n):
# type: (str, int) -> str
encoded = [_encode_char(char, n) for char in s] # type: List[str]
return ''.join(encoded)
def decode(s, n):
# type: (str, int) -> str
decoded = [_decode_char(char, n) for char in s] # type: List[str]
return ''.join(decoded)
def _encode_char(char, n):
# type: (str, int) -> str
if char not in characters:
return char
original_index = characters.index(char) # type: int
new_index = original_index + n # type: int
fixed_index = new_index % count # type: int
return characters[fixed_index]
def _decode_char(char, n):
# type: (str, int) -> str
return _encode_char(char, -n)
def test_encode_char():
# type: () -> None
assert _encode_char('a', 1) == 'b'
assert _encode_char('z', 1) == 'A'
assert _encode_char('Z', 1) == '0'
assert _encode_char('9', 1) == 'a'
assert _encode_char('a', len(characters) + 1) == 'b'
assert _encode_char('z', len(characters) + 1) == 'A'
assert _encode_char('Z', len(characters) + 1) == '0'
assert _encode_char('9', len(characters) + 1) == 'a'
def test_decode_char():
# type: () -> None
assert _decode_char('b', 1) == 'a'
assert _decode_char('A', 1) == 'z'
assert _decode_char('0', 1) == 'Z'
assert _decode_char('a', 1) == '9'
assert _decode_char('b', len(characters) + 1) == 'a'
assert _decode_char('A', len(characters) + 1) == 'z'
assert _decode_char('0', len(characters) + 1) == 'Z'
assert _decode_char('a', len(characters) + 1) == '9'
def test_string_encode_decode():
# type: () -> None
assert encode('azZ9', 1) == 'bA0a'
assert decode('bA0a', 1) == 'azZ9'
s = "Th3 qu1ck br0wn f0x jump5 over the lazy dog, and away he goes!" # type: str
assert decode(encode(s, 1), 1) == s
assert decode(encode(s, 10), 10) == s
assert decode(encode(s, 100), 100) == s
Annotations
- New Python syntax
- Introduced in Python3.5
- Standard python syntax checker will complain if you get something wrong
Example
from typing import List
def hello() -> None:
print('hello')
class Example:
def method(self, lst: List[str], opt: int=0, *args: str, **kwargs: bool) -> int:
"""dont give a type to self
note * and **
"""
etc
@classmethod
def cls_methodss(cls, lst: List[int]) -> int:
"""dont give a type to cls"""
etc
Excercise
Add type annotations to caesar.py
mypy --py2 program.py
# or
mypy program.py
Answer
import string
from typing import List
characters: str = string.ascii_letters + string.digits
count: int = len(characters)
def encode(s: str, n: int) -> str:
encoded: List[str] = [_encode_char(char, n) for char in s]
return ''.join(encoded)
def decode(s: str, n: int) -> str:
decoded: List[str] = [_decode_char(char, n) for char in s]
return ''.join(decoded)
def _encode_char(char: str, n: int) -> str:
if char not in characters:
return char
original_index: int = characters.index(char)
new_index: int = original_index + n
fixed_index: int = new_index % count
return characters[fixed_index]
def _decode_char(char: str, n: int) -> str:
return _encode_char(char, -n)
def test_encode_char():
assert _encode_char('a', 1) == 'b'
assert _encode_char('z', 1) == 'A'
assert _encode_char('Z', 1) == '0'
assert _encode_char('9', 1) == 'a'
assert _encode_char('a', len(characters) + 1) == 'b'
assert _encode_char('z', len(characters) + 1) == 'A'
assert _encode_char('Z', len(characters) + 1) == '0'
assert _encode_char('9', len(characters) + 1) == 'a'
def test_decode_char():
assert _decode_char('b', 1) == 'a'
assert _decode_char('A', 1) == 'z'
assert _decode_char('0', 1) == 'Z'
assert _decode_char('a', 1) == '9'
assert _decode_char('b', len(characters) + 1) == 'a'
assert _decode_char('A', len(characters) + 1) == 'z'
assert _decode_char('0', len(characters) + 1) == 'Z'
assert _decode_char('a', len(characters) + 1) == '9'
def test_string_encode_decode():
assert encode('azZ9', 1) == 'bA0a'
assert decode('bA0a', 1) == 'azZ9'
s = "Th3 qu1ck br0wn f0x jump5 over the lazy dog, and away he goes!"
assert decode(encode(s, 1), 1) == s
assert decode(encode(s, 10), 10) == s
assert decode(encode(s, 100), 100) == s
Complex Types
Sometimes a variable/parameter/return has multiple valid types...
Introducing Union
from typing import Union
# union can have >= 1 types
a: Union[int,float,str] = 1
b: Union[int,float,str] = 2.22
c: Union[int,float,str] = 'hello'
d: Union[int,float,str] = None # Error
# a union of unions is a union
Union[Union[int,str],Union[int,float],None]
# is equivalent to
Union[int,float,str,None]
# Unions can be used in function definitions just like normal types
def foo(a:Union[int,float]) -> Union[int,float,str,None]:
etc
Introducing Dict
from typing import Dict
d1: Dict[KeyType,ValType] = {x:y}
d2: Dict[int,str] = {1:'a',2:'b'}
Introducing reveal_type
i = 0
s = '0'
reveal_type(i)
# mypy: error - Revealed type is 'builtins.int'
# python: NameError: name 'reveal_type' is not defined
reveal_type(s)
# mypy: error - Revealed type is 'builtins.str'
Excercise
from typing import Dict, Union
d1: Dict[int,str] = {1:'a',2:'b'}
d2 = {'a':1,'b':2}
d3 = {'a':1,'b':'hi there'}
d4 = {
1 : 'a' , 2 : 222222 , 3 : 'c' , 5 : 12.3 , 6: 'f'
}
dict_and_union.py
Excercise
database = {
1 : 'a' , 2 : 222222 , 3 : 'c' , 5 : 12.3 , 6: 'f'
}
def get_by_id(id):
if id in database:
return database[id]
return None
def print_it_n_times(s,n):
print(s*n)
value = get_by_id(4)
print_it_n_times(value,4)
database.py
Run the code
Traceback (most recent call last):
File "optional0.py", line 15, in <module>
print_it_n_times(value,4)
File "optional0.py", line 11, in print_it_n_times
print(s*n)
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'
Add annotations...
from typing import List, Dict, Union
database: Dict[int,Union[str,int,float]] = {
1 : 'a' , 2 : 222222 , 3 : 'c' , 5 : 12.3 , 6: 'f'
}
def get_by_id(id:int)-> Union[str,int,float,None]:
if id in database:
return database[id]
return None
def print_it_n_times(s:Union[str,int,float],n:int) -> None:
print(s*n)
value = get_by_id(4)
reveal_type(value) ###
# Revealed type is 'Union[builtins.str, builtins.int, builtins.float, None]'
print_it_n_times(value,4)
# error: Argument 1 to "print_string_n_times" has incompatible type "Union[builtins.str, builtins.int, builtins.float, None]"; expected "str"
Fix the bug ...
from typing import List,Dict, Union
database: Dict[int,Union[str,int,float]] = {
1 : 'a' , 2 : 222222 , 3 : 'c' , 5 : 12.3 , 6: 'f'
}
def get_by_id(id:int)-> Union[str,int,float,None]:
if id in database:
return database[id]
return None
def print_it_n_times(s:Union[str,int,float],n:int) -> None:
print(s*n)
value = get_by_id(4)
reveal_type(value)
# Revealed type is 'Union[builtins.str, builtins.int, builtins.float, None]'
if value is not None:
reveal_type(value)
# Revealed type is 'Union[builtins.str, builtins.float]'
print_it_n_times(value,4)
Introducing aliases
from typing import List, Dict, Union
# make it DRY with an alias
DbValueType = Union[str,int,float]
database: Dict[int,DbValueType] = {
1 : 'a' , 2 : 222222 , 3 : 'c' , 5 : 12.3 , 6: 'f'
}
Introducing aliases
from typing import List,Dict, Union
# make it DRY with an alias
DbValueType = Union[str,int,float]
database: Dict[int,DbValueType] = {
1 : 'a' , 2 : 222222 , 3 : 'c' , 5 : 12.3 , 6: 'f'
}
def get_by_id(id:int)-> Union[DbValueType,None]:
if id in database:
return database[id]
return None
def print_it_n_times(s:DbValueType,n:int) -> None:
print(s*n)
value = get_by_id(4)
reveal_type(value)
# Revealed type is 'Union[builtins.str, builtins.int, builtins.float, None]'
if value is not None:
reveal_type(value)
# Revealed type is 'Union[builtins.str, builtins.float]'
print_it_n_times(value,4)
Introducing Optional
from typing import Union,Optional
DbValueType = Union[str,int,float]
OptionalDbValueType = Union[DbValueType,None]
# equivalent to
OptionalDbValueType = Optional[DbValueType]
Introducing Optional
from typing import List,Dict, Union,Optional
# make it DRY with an alias
DbValueType = Union[str,int,float]
database: Dict[int,DbValueType] = {
1 : 'a' , 2 : 222222 , 3 : 'c' , 5 : 12.3 , 6: 'f'
}
def get_by_id(id:int)-> Optional[DbValueType]:
if id in database:
return database[id]
return None
def print_it_n_times(s:DbValueType,n:int) -> None:
print(s*n)
value = get_by_id(4)
reveal_type(value)
# Revealed type is 'Union[builtins.str, builtins.int, builtins.float, None]'
if value is not None:
reveal_type(value)
# Revealed type is 'Union[builtins.str, builtins.float]'
print_it_n_times(value,4)
In Summary
- reveal_type tells us the type of a variable. Must be removed from final code
- Union lets us specify a list of valid types
- Types can be assigned to variables (called aliases)
- Optional[T] == Union[T,None]
Generics
Sometimes types are determined when a function is envoked or a class is instantiated
Generic functions
from typing import TypeVar, List
T1 = TypeVar('T1')
T2 = TypeVar('T2')
def cheese(input: List[T1]) -> T1:
etc
# you can have multiple typevars
def spam(eggs:Dict[T1:T2],parrot:T1)-> T2:
etc
Generic functions
from typing import TypeVar, Sequence, Optional
T = TypeVar('T') # Declare type variable
def first(seq: Sequence[T]) -> T:
return seq[0]
def last(seq: Sequence[T]) -> T:
return seq[-1]
a = first([1,2,3])
reveal_type(a) # int
b = last([1,2,3])
reveal_type(b) # int
c = first(['a','b','c'])
reveal_type(c) # str
d = last(['a','b','c'])
reveal_type(d) # str
Generic classes
from typing import TypeVar, List, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
# Create an empty list with items of type T
self.items: List[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
def empty(self) -> bool:
return not self.items
stack : Stack[int] = Stack()
# equivalent to
stack = Stack[int]()
stack.push(2)
stack.pop()
stack.push('x') #error
Bind generics to specific types
from typing import TypeVar, Generic
class Fruit:
pass
class Apple(Fruit):
pass
class RaceCar:
pass
TFruit = TypeVar('TFruit',bound=Fruit)
class FruitTree(Generic[TFruit]):
pass
x = FruitTree[Fruit]()
y = FruitTree[Apple]()
z = FruitTree[RaceCar]() # Error
Excercise
class Queue:
def __init__(self) -> None:
self.items = []
def push(self, item):
self.items.append(item)
def next(self):
return self.items.pop(0)
def peek(self):
if self.items:
return self.items[0]
int_queue = Queue()
int_queue.push(1)
int_queue.push("2") # should error
str_queue = Queue()
str_queue.push("1")
str_queue.push(2) # should error
generics.py
Answer
from typing import TypeVar, Generic, List, Optional
T = TypeVar('T')
class Queue(Generic[T]):
def __init__(self) -> None:
self.items: List[T] = []
def push(self, item:T) -> None:
self.items.append(item)
def next(self) -> T:
return self.items.pop(0)
def peek(self) -> Optional[T]:
if self.items:
return self.items[0]
return None
int_queue: Queue[int] = Queue()
int_queue.push(1)
int_queue.push("2") # Error: expected int
str_queue = Queue[str]()
str_queue.push("1")
str_queue.push(2) # Error: expected str
Excercise:
Alter queue so that it only allows booleans, strings and integers
Answer
from typing import TypeVar, Generic, List, Optional,Union
T = TypeVar('T',bound=Union[bool,str,int])
class Queue(Generic[T]):
def __init__(self) -> None:
self.items: List[T] = []
def push(self, item:T) -> None:
self.items.append(item)
def next(self) -> T:
return self.items.pop(0)
def peek(self) -> Optional[T]:
if self.items:
return self.items[0]
return None
int_queue: Queue[int] = Queue()
int_queue.push(1)
int_queue.push("2") # Error: expected int
str_queue = Queue[str]()
str_queue.push("1")
str_queue.push(2) # Error: expected str
Overloading
Sometimes a functions can have different argument types in different situations...
example
import random
from typing import List
from typing_extensions import Protocol
class SupportsIsValid(Protocol):
def is_valid(self) -> bool:
...
class Foo:
def is_valid(self) -> bool:
return random.choice([True,False])
class Bar:
def is_valid(self) -> bool:
return random.choice([True,False])
def valid_instances(
l: List[SupportsIsValid],
validator=None) -> List[SupportsIsValid]:
if validator is not None:
return [x for x in l if validator(x)]
return [x for x in l if x.is_valid()]
l: List[SupportsIsValid] = []
l.extend([Foo() for _ in range(3)])
l.extend([Bar() for _ in range(3)])
valid = valid_instances(l)
add annotations...
import random
from typing import TypeVar, List, Callable,overload,Optional
from typing_extensions import Protocol
class SupportsIsValid(Protocol):
def is_valid(self) -> bool:
...
class Foo:
def is_valid(self) -> bool:
return random.choice([True,False])
class Bar:
def is_valid(self) -> bool:
return random.choice([True,False])
T = TypeVar('T')
@overload
def valid_instances(l: List[T], validator: Callable[[T],bool]) -> List[T]:
...
@overload
def valid_instances(l: List[SupportsIsValid])-> List[SupportsIsValid]:
...
def valid_instances( l:List[T], validator:Optional[Callable[[T],bool]]=None ) -> List[T]:
if validator is not None:
return [x for x in l if validator(x)]
return [x for x in l if x.is_valid()]
l: List[SupportsIsValid] = []
l.extend([Foo() for _ in range(3)])
l.extend([Bar() for _ in range(3)])
valid = valid_instances(l)
Excercise
from typing import Union, Optional
class Event:
def __init__(self,*args):
pass
class ClickEvent(Event): pass
class DragEvent(Event): pass
def mouse_event(x1: int,
y1: int,
x2: Optional[int] = None,
y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]:
if x2 is None and y2 is None:
return ClickEvent(x1, y1)
elif x2 is not None and y2 is not None:
return DragEvent(x1, y1, x2, y2)
else:
raise TypeError("Bad arguments")
reveal_type(mouse_event(1,2)) # Union[ClickEvent, DragEvent]
reveal_type(mouse_event(1,2,3)) # Union[ClickEvent, DragEvent]
reveal_type(mouse_event(1,2,3,4)) # Union[ClickEvent, DragEvent]
overload.py
Answer
from typing import Union, Optional, overload
class Event:
def __init__(self,*args):
pass
class ClickEvent(Event): pass
class DragEvent(Event): pass
@overload
def mouse_event(x1: int, y1: int) -> ClickEvent:
...
@overload
def mouse_event(x1: int, y1: int, x2: int, y2: int) -> DragEvent:
...
def mouse_event(x1: int,
y1: int,
x2: Optional[int] = None,
y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]:
if x2 is None and y2 is None:
return ClickEvent(x1, y1)
elif x2 is not None and y2 is not None:
return DragEvent(x1, y1, x2, y2)
else:
raise TypeError("Bad arguments")
reveal_type(mouse_event(1,2)) # ClickEvent
reveal_type(mouse_event(1,2,3)) # No overload varient match
reveal_type(mouse_event(1,2,3,4)) # DragEvent