Hello Types


https://pres-py-types-tutorial-dot-prelude-za.appspot.com

Ice breaker


Me :)

Contents

  • Setup
  • Why type? Why not?
  • MyPy and Typing
  • Type hint notations
  • Advanced type hints
  • Where to from here?

Setup

Prerequisites

Python 3.6+

python3 -m pip install mypy
python3 -m pip install typingextensions
                    
Python 2.7

python -m pip install typing
                    

Links

Why type? Why not?

  • removes many runtime bugs
  • Explicit is better than implicit
  • Self documenting code
  • Dont get out of date like docstrings
  • Gradual typing, not static typing
  • Slows down the coding process?? Just like unit tests and sensable naming conventions, right?

Mypy and typing

  • mypy is considered the "reference implementation" for python typing
  • a lot of what is now standard practice is borrowed from mypy
  • static analyser (think linter)
  • command line or integrated into editor
  • MyPy Runs on Python 3.4+
  • Type annotations are a part of Python3.5+
  • Python will remain a dynamically typed language!

mypy alternatives

Type hint notations

  • Comments
  • Annotations
  • Stub Files

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

Recent development
New syntax added in Python 3.6!


                        # < 3.6
                        foo = [] # type: List[str]

                        # >= 3.6
                        bar: List[str] = []

                    
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

    

Stub files

  • a description of the public interface of the module without any implementations (think header file)
  • pyi files
  • can be alongside modules (csv.pyi and csv.py) or in a seperate configurable location
  • If both a .py and a .pyi file for the same module contain type hints, the .pyi file takes precedence
  • allows adding type hints to source without touching the source
Mypy and Typeshed
  • Typeshed is a stub only package
  • Contains stubs for std lib and select 3rd party packages
  • MyPy package contains specific version of Typeshed
functions and variables

x: int

def afunc(code: str) -> int: ...
def anotherfunc(a: int, b: int = ...) -> int: ...
                    
Classes (jinja2 stub)

class ProcessedTraceback:
    exc_type = ...  # type: Any
    exc_value = ...  # type: Any
    frames = ...  # type: Any
    def __init__(self, exc_type, exc_value, frames) -> None: ...
    def render_as_text(self, limit: Optional[int] = ...) -> str: ...
Jinja2 code



class ProcessedTraceback(object):
    """Holds a Jinja preprocessed traceback for printing or reraising."""

    def __init__(self, exc_type, exc_value, frames):
        assert frames, 'no frames for this traceback?'
        self.exc_type = exc_type
        self.exc_value = exc_value
        self.frames = frames

        # newly concatenate the frames (which are proxies)
        prev_tb = None
        for tb in self.frames:
            if prev_tb is not None:
                prev_tb.set_next(tb)
            prev_tb = tb
        prev_tb.set_next(None)

    def render_as_text(self, limit=None):
        """Return a string with the traceback."""
        lines = traceback.format_exception(self.exc_type, self.exc_value,
                                           self.frames[0], limit=limit)
        return ''.join(lines).rstrip()


                 

Choices Choices


if can_edit_source_code:
    if must_support_python_2:
        return use_comments
    elif must_support_less_than_py_36:
        return use_annotations_but_be_careful
    else:
        return use_annotations_to_your_hearts_content
else:
    return use_stubs
                    

Advanced type hints


from typing import TheBigGuns
                    

class Bee !!!

All bees know at all times:
  • how many bees there are in the swarm
  • the identities of all bees in the swarm

Also, bees collect and deposit pollen
bees.py

class Bee:
    all_bees   = []
    bee_count  = 0
    has_pollen = False

    def __init__(self):
        self.all_bees.append(self)
        self.bee_count += 1

    def collect_pollen(self):
        self.has_pollen = True

    def deposit_pollen(self):
        self.has_pollen = False

def test_bee():
    bees = [Bee() for i in range(3)]
    for bee in bees:
        assert bee.all_bees == bees
        assert bee.bee_count == 3
                    
bees.py
Failure!

====================================================== FAILURES ======================================================
______________________________________________________ test_bee ______________________________________________________

    def test_bee():
        bees = [Bee() for i in range(3)]
        for bee in bees:
            assert bee.all_bees == bees
>           assert bee.bee_count == 3
E           assert 1 == 3
E            +  where 1 = <bees0.Bee object at 0x7fe1ef395cf8>.bee_count

bees0.py:12: AssertionError
============================================== 1 failed in 0.10 seconds ==============================================
add annotations...

from typing import List

class Bee:
    all_bees: List["Bee"] = []   ######
    etc

add annotations...

from typing import List

class Bee:
    all_bees: List["Bee"] = []   ######
    bee_count: int        = 0
    has_pollen: bool      = False

    def __init__(self) -> None:
        self.all_bees.append(self)
        self.bee_count += 1

    def get_pollen(self) -> None:
        self.has_pollen = True

    def deposit_pollen(self) -> None:
        self.has_pollen = False

def test_bee():
    bees = [Bee() for i in range(3)]
    for bee in bees:
        assert bee.all_bees == bees
        assert bee.bee_count == 3

Introducing ClassVar

from typing import List, ClassVar

class Bee:
    all_bees: ClassVar[List["Bee"]] = []
    bee_count: ClassVar[int]        = 0
    has_pollen: bool                = False # No classvar

    def __init__(self) -> None:
        self.all_bees.append(self)
        self.bee_count += 1
        # error: Cannot assign to class variable "bee_count" via instance

    def get_pollen(self) -> None:
        self.has_pollen = True

    def deposit_pollen(self) -> None:
        self.has_pollen = False

def test_bee():
    bees = [Bee() for i in range(3)]
    for bee in bees:
        assert bee.all_bees == bees
        assert bee.bee_count == 3

Final Version

from typing import List, ClassVar

class Bee:
    all_bees: ClassVar[List["Bee"]] = []
    bee_count: ClassVar[int]        = 0
    has_pollen: bool                = False # No classvar

    def __init__(self) -> None:
        self.all_bees.append(self) ####
        Bee.bee_count += 1         ####

    def get_pollen(self) -> None:
        self.has_pollen = True

    def deposit_pollen(self) -> None:
        self.has_pollen = False

def test_bee():
    bees = [Bee() for i in range(3)]
    for bee in bees:
        assert bee.all_bees == bees
        assert bee.bee_count == 3

Excercise

from typing import ClassVar

class Spam:
    egg: ClassVar[int] = 5
    cheese: str = "cheddar"

inst1 = Spam()
inst2 = Spam()

# what will be printed?
# are there any type errors?

print(inst1.egg)
print(inst1.cheese)

Spam.cheese = "gouda"
inst1.cheese = "parmasan"

print(inst1.cheese)
print(inst2.cheese)
print(Spam.cheese)

Spam.egg    = "poached"
inst1.egg   = 12
Spam.cheese = 12
classvar.py

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

Protocols

If it walks like a duck and quacks like a duck...
... it's a duck


class RubberDuck:
    def quack(self):
        return "squeak"

class MallardDuck:
    def quack(self):
        return "quack"

def all_the_quacks(all_the_ducks):
    for o in all_the_ducks:
        yield o.quack()


l = []
l.extend([RubberDuck() for _ in range(3)])
l.extend([MallardDuck() for _ in range(3)])

all_the_quacks(l)

add annotations...


from typing import TypeVar, List, Iterator
from typing_extensions import Protocol

class SupportsQuack(Protocol):
    def quack(self)-> str:
        ...

class RubberDuck:
    def quack(self) -> str:
        return "squeak"

class MallardDuck:
    def quack(self) -> str:
        return "quack"

def all_the_quacks(all_the_ducks:List[SupportsQuack]) -> Iterator[str]:
    for o in all_the_ducks:
        yield o.quack()

l: List[SupportsQuack] = []
l.extend([RubberDuck() for _ in range(3)])
l.extend([MallardDuck() for _ in range(3)])

all_the_quacks(l)

Excercise


import random

class Foo:
    def is_valid(self):
        return random.choice([True,False])

class Bar:
    def is_valid(self):
        return random.choice([True,False])

def valid_instances(l):
    return [x for x in l if x.is_valid()]

l = []
l.extend([Foo() for _ in range(3)])
l.extend([Bar() for _ in range(3)])

valid = valid_instances(l)

    
protocols.py
Answer


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]) -> List[SupportsIsValid]:
    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)

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

    

Where to from here?

Adding types to exiting code base

Oh Crap

Common issues

Mypy workflows

Runtime type checking

THE END