Using Type Annotations in Python by Philippe Fremy / IDEMIA Python - - PowerPoint PPT Presentation

using type annotations in python
SMART_READER_LITE
LIVE PREVIEW

Using Type Annotations in Python by Philippe Fremy / IDEMIA Python - - PowerPoint PPT Presentation

Using Type Annotations in Python by Philippe Fremy / IDEMIA Python code can be obscure def validate(form, data): """Validates the input data""" return form.validate(data) You do not know the types of the


slide-1
SLIDE 1

Using Type Annotations in Python

by Philippe Fremy / IDEMIA

slide-2
SLIDE 2

Python code can be obscure

  • You do not know the types of the arguments
  • The function may accept multiple types and you don’t know it
  • Docstrings (when present) may not be accurate or useful
  • You may break production code just be providing an unexpected type

and you will only notice it at run-time.

def validate(form, data): """Validates the input data""" return form.validate(data)

slide-3
SLIDE 3

Very brief history of type annotations

  • Function argument annotations introduced for Python 3.0 in 2006 by

Guido van Rossum

  • Type annotations introduced in Python 3.5 (2015)
  • Further improved in Python 3.6 (2016) and Python 3.7 (2018)
slide-4
SLIDE 4

Syntax for type annotation

# a function def my_func(a : int, b : str = "") -> bool: # ... # a method class A: def my_method(self, a : bool, b : int = 0) -> None: # ...

slide-5
SLIDE 5

# Variables (only with python 3.6 and later) a: int = 0 b: str class MyClass: c: float # type of the instance variable # (only with python 3.6 and later) def __init__(self) -> None: self.c = 33.17 self.d: str = "I am a string"

Syntax for type annotation

slide-6
SLIDE 6

Available types for annotations

# List defines a general content type my_list_int: List[int] = [1,2,3] # multiple types content requires Union my_multi_type_list: List[ Union[bool, int] ] = [ True, 33 ] # Tuple usually define precisely all their members my_tuple: Tuple[int, str, float] = (1, "abc", 3.14) # Tuple can also declare a general content type my_float_tuple: Tuple[float, ...] = (11.14, 20.18, 0.1)

slide-7
SLIDE 7

Available types for annotations

# Dict defines keys and content type my_dict: Dict[str, int] = { "33": 17 } # Containers may be combined school_coords: Dict[ str, Tuple[int, int] ] school_coords = {"Epita": (10, 20)}

slide-8
SLIDE 8

# None is a valid type annotation def f(a: None) -> int: ... # None is always used in a Union: def f(a: Union[None, int]) -> int: ... # Union[None, int] may be spelled as Optional[int] def f(a: Optional[int] = None) -> int: ...

Available types for annotations

slide-9
SLIDE 9

And there is more…

The typing module also offers :

  • Duck typing with types such as Sequence, Mapping, Iterable, Sized, ...
  • Type aliasing, type generics, subtyping, typing joker with Any, …
  • Conversion between types with cast

Please check the typing module documentation and the Mypy tool

slide-10
SLIDE 10

How does Python handle type annotations ?

  • Annotations are valid expressions evaluated during module loading
  • Result is stored in the function object
  • And then … they are totally ignored by Python

Type annotations are verified by external tools : Mypy, Pyre, …

slide-11
SLIDE 11

Type Annotations verification tools

Tools to verify static type information:

  • PyCharm IDE along with inspection mode
  • Mypy : Open Source, written in Python, maintained by Dropbox team on GitHub
  • Pyre : Open Source, written in OCaml, maintained by Facebook team on GitHub, only for

Linux and MacOs X

slide-12
SLIDE 12

How to get started with annotations

  • On a new codebase set the rule of having annotations and be strict about it.
  • On an existing codebase, start small, one module at a time.

Then improve gradually. All the annotation tools are designed for gradual improvements.

  • Put static type verification in your Continuous Integration / Nightly builds / non

regression tests.

slide-13
SLIDE 13

Proceed one module at a time

Step 1: add annotations to my_module.py and verify them Mypy in strict mode complains about every missing annotation. $ mypy --strict my_module.py my_module.py:11: error: Function is missing a return type annotation

slide-14
SLIDE 14

Proceed one module at a time

Step 1: add annotations to my_module.py and verify them Mypy in strict mode complains about every missing annotation. Step 2: when the module is fully annotated, check the whole codebase. Mypy reports every misuse of my_module (only in annotated code). $ mypy --strict my_module.py my_module.py:11: error: Function is missing a return type annotation $ mypy *.py mod2.py:5: error: Argument 1 to "my_func" has incompatible type "float"; expected "int"

slide-15
SLIDE 15

Proceed one module at a time

Step 1: add annotations to my_module.py and verify them Mypy in strict mode complains about every missing annotation. Step 2: when the module is fully annotated, check the whole codebase. Mypy reports every misuse of my_module (only in annotated code). Step 3: run your non-regression tests $ mypy --strict my_module.py my_module.py:11: error: Function is missing a return type annotation $ mypy *.py mod2.py:5: error: Argument 1 to "my_func" has incompatible type "float"; expected "int"

slide-16
SLIDE 16

Where to add type annotation

# annotate all your functions and methods # variable with value do not need type annotation vat_rate = 20 # OK, vat_rate is an int # unless the value type is not correct… if reduced_vat: vat_rate = 5.5 # Error from mypy, vat_rate does not accept float vat_rate: float = 20 # OK for float and int values

slide-17
SLIDE 17

Where to add type annotations

# All empty containers need annotations names = [] # Mypy can not figure out the content type names: List[str] = [] # OK # Dict and other empty containers need annotations birth_dates: Dict[str, Date] birth_dates = {}

slide-18
SLIDE 18

Let’s practice

Example 1

slide-19
SLIDE 19

class A: def use_another_a(self, a: A) -> None: pass def use_b(self, b: Optional[B]) -> None: pass class B: pass

slide-20
SLIDE 20

$ mypy --strict ab.py $ $ python ab.py File “ab.py", line 4, in A def use_another_a( self, a: A ) -> None: NameError: name 'A' is not defined File “ab.py", line 7, in A def use_b( self, b: Optional[B] ) -> None: NameError: name 'B' is not defined class A: def use_another_a(self, a: A) -> None: pass def use_b(self, b: Optional[B]) -> None: pass class B: pass

slide-21
SLIDE 21

from __future__ import annotations # python 3.7 only class A: def use_another_a(self, a: A) -> None: pass def use_b(self, b: Optional[B]) -> None: pass class B: pass $ mypy --strict ab.py $ $ python ab.py $

slide-22
SLIDE 22

# Other solution: put annotations inside quotes class A: def use_another_a(self, a: "A") -> None: pass def use_b(self, b: Optional["B"]) -> None: pass class B: pass $ mypy --strict ab.py $ $ python ab.py $

slide-23
SLIDE 23

Let’s practice

Example 2

slide-24
SLIDE 24

class A: def __init__(self, step_init: Optional[int] = None) -> None: self.step = step_init def get_step(self) -> int: return self.step + 1

slide-25
SLIDE 25

class A: def __init__(self, step_init: Optional[int] = None) -> None: self.step = step_init def get_step(self) -> int: return self.step + 1 $ mypy --strict a.py a.py:6: error: Unsupported operand types for + ("Optional[int]" and "int")

slide-26
SLIDE 26

class A: def __init__(self, step_init: Optional[int] = None) -> None: self.step = step_init def get_step(self) -> int: return self.step + 1 $ mypy --strict a.py a.py:6: error: Unsupported operand types for + ("Optional[int]" and "int") Mypy found a bug !

slide-27
SLIDE 27

# Solution 1: prepend a check for None class A: def __init__(self, step_init: Optional[int] = None) -> None: self.step = step_init def get_step(self) -> int: # deal with self.step being None if self.step is None: return 0 # now we can proceed return self.step + 1 $ mypy --strict a.py $

slide-28
SLIDE 28

# Solution 2: default initialise with the right type class A: def __init__(self, step_init: Optional[int] = None) -> None: self.step = step_init or 0 # self.step type is always int def use_step(self) -> int: return self.step + 1 $ mypy --strict a.py $

slide-29
SLIDE 29

# Solution 3: do not use Optional, have better default class A: def __init__(self, step_init: int = 0) -> None: self.step = step_init def get_step(self) -> int: return self.step + 1 $ mypy --strict a.py $

slide-30
SLIDE 30

# Solution 4: disable None checking in Mypy class A: def __init__(self, step_init: Optional[int] = None) -> None: self.step = step_init def get_step(self) -> int: return self.step + 1 $ mypy --strict --no-strict-optional a.py $

slide-31
SLIDE 31

# Solution 5: silence the error (not a good practice) class A: def __init__(self, step_init: Optional[int] = None) -> None: self.step = step_init def get_step(self) -> int: return self.step + 1 # type: ignore $ mypy --strict a.py $

slide-32
SLIDE 32

Let’s practice

Example 3

slide-33
SLIDE 33

# Dealing with multiple types def upper(thing: Union[str, bytes, List[str]]) -> str: if type(thing) == list: thing = "".join(thing) return thing.upper() $ mypy --strict upper.py upper.py:5: error: Argument 1 to "join" of "str" has incompatible type "Union[str, bytes, List[str]]"; expected "Iterable[str]" upper.py:8: error: Incompatible return value type (got "Union[str, bytes, List[str]]", expected "str")

slide-34
SLIDE 34

# Dealing with multiple types def upper(thing: Union[str, bytes, List[str]]) -> str: if type(thing) == list: thing = "".join(thing) return thing.upper()

slide-35
SLIDE 35

# Dealing with multiple types def upper(thing: Union[str, bytes, List[str]]) -> str: if type(thing) == list: thing = "".join(thing) return thing.upper() $ mypy --strict upper.py upper.py:5: error: Argument 1 to "join" of "str" has incompatible type "Union[str, bytes, List[str]]"; expected "Iterable[str]" upper.py:8: error: Incompatible return value type (got "Union[str, bytes, List[str]]", expected "str")

slide-36
SLIDE 36

# Solution: use isinstance() def upper(thing: Union[str, bytes, List[str]]) -> str: if isinstance(thing, list): # mypy understand isinstance() thing = "".join(thing) # so now, join() passes fine return thing.upper()

slide-37
SLIDE 37

Mypy found a bug ! I forgot to deal with bytes $ mypy --strict upper.py upper.py:7: error: Incompatible return value type (got "Union[str, bytes]", expected "str") # Solution: use isinstance() def upper(thing: Union[str, bytes, List[str]]) -> str: if isinstance(thing, list): # mypy understand isinstance() thing = "".join(thing) # so now, join() passes fine return thing.upper()

slide-38
SLIDE 38

Mypy found a bug ! I forgot to deal with bytes $ mypy --strict upper.py upper.py:7: error: Incompatible return value type (got "Union[str, bytes]", expected "str") # Solution: use isinstance() def upper(thing: Union[str, bytes, List[str]]) -> str: if isinstance(thing, list): # mypy understand isinstance() thing = "".join(thing) # so now, join() passes fine return thing.upper()

slide-39
SLIDE 39

$ mypy --strict upper.py $ # Solution: use isinstance() and catch all types def upper(thing: Union[str, bytes, List[str]]) -> str: if isinstance(thing, list): thing = "".join(thing) elif isinstance(thing, bytes): # we also check for bytes thing = thing.decode("UTF8") # now, all paths make thing a string return thing.upper() # OK, returning a str

slide-40
SLIDE 40

$ mypy --strict upper.py $ # Solution: use cast and catch all types def upper(thing: Union[str, bytes, List[str]]) -> str: if type(thing) == list: thing = cast(List[str], thing) thing = "".join(thing) elif type(thing) == bytes: thing = cast(bytes, thing) thing = thing.decode("UTF8") thing = cast(str, thing) return thing.upper()

slide-41
SLIDE 41

Let’s practice

Example 4

slide-42
SLIDE 42

# file form_validator.py def validate(form, data): # ... (do some pre-validation stuff) return form.validate(data) class UserForm: def validate(self, data): """Validates the data. Data must be a list of int""" data[4] = data[1] * data[2] % data[3] return data[4] > 21

slide-43
SLIDE 43

# file production_code.py def production_code(): userForm = UserForm() data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # ... return validate(userForm, data)

slide-44
SLIDE 44

# file production_code.py def production_code(): userForm = UserForm() # data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] data = range(10) # ... return validate(userForm, data)

slide-45
SLIDE 45

# file production_code.py def production_code(): userForm = UserForm() # data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] data = range(10) # ... return validate(userForm, data) $ python production_code.py Traceback (most recent call last): File "production_code.py", line 7, in <module> production_code() File "form_validator.py", line 4, in validate return form.validate(data) File "form_validator.py", line 9, in validate data[4] = data[1] * data[2] % data[3] TypeError: 'range' object does not support item assignment

slide-46
SLIDE 46

# file form_validator.py def validate(form: UserForm, data: List[int]): # ... (do some pre-validation stuff) return form.validate(data) class UserForm: def validate(self, data: List[int]): """Validates the data. Data must be a list of int""" data[4] = data[1] * data[2] % data[3] return data[4] > 21

slide-47
SLIDE 47

# file production_code.py def production_code() -> bool: userForm = UserForm() # data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] data = range(10) # ... return validate(userForm, data)

slide-48
SLIDE 48

# file production_code.py def production_code() -> bool: userForm = UserForm() # data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] data = range(10) # ... return validate(userForm, data) $ mypy production_code.py production_code.py:7: error: Argument 1 to "validate" has incompatible type "range"; expected "List[int]“

slide-49
SLIDE 49

# file production_code.py def some_production_code() -> bool: userForm = UserForm() # data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] data = list(range(10)) # ... return validate(userForm, data) $ mypy production_code.py $ $ python production_code.py …

slide-50
SLIDE 50

Let’s practice

Monkeytype

slide-51
SLIDE 51

Let the monkey find the types for you

def validate(form, data): """Validates the input data""" return form.validate(data)

slide-52
SLIDE 52

Let the monkey find the types for you

$ monkeytype run all_unit_tests.py $ monkeytype run end_to_end_tests.py $ monkeytype run production_code.py $ monkeytype apply validate def validate(form, data): """Validates the input data""" return form.validate(data)

slide-53
SLIDE 53

Let the monkey find the types for you

You can also use PyAnnotate which does the same thing.

$ monkeytype run all_unit_tests.py $ monkeytype run end_to_end_tests.py $ monkeytype run production_code.py $ monkeytype apply validate def validate(form, data): """Validates the input data""" return form.validate(data) def validate(form: Union[UserForm, AdminForm], data: List[int]) -> bool: """Validates the input data""" return form.validate(data)

slide-54
SLIDE 54

Conclusion

  • Type annotation is powerful to bug finder. Use it !
  • Type annotation is also good way of documenting your code
  • Feedback from developers using type annotation: “It rocks !”
  • Some Python dynamic constructs are difficult to verify statically

That’s why you should go step-by-step when adding annotations Mypy has excellent documentation to complement this presentation

  • Tools like MonkeyType or PyAnnotate can really help.
slide-55
SLIDE 55

Philippe Fremy / IDEMIA in Bordeaux

(IDEMIA is recruiting)

philippe.fremy@idemia.com Slides are online at PyParis website and at: https://github.com/bluebird75/whoiam

slide-56
SLIDE 56

Time for questions

slide-57
SLIDE 57

Let’s practice - extra track

Example 5 It did not fit into the 40 minutes track

slide-58
SLIDE 58

# file a.py class A: # ... # file b.py from a import A class B(A): pass

slide-59
SLIDE 59

# file a.py class A: def use_some_b(self, b): pass # file b.py from a import A class B(A): pass

slide-60
SLIDE 60

# file b.py from a import A class B(A): pass # file a.py class A: def use_some_b(self, b: B) -> None: pass

slide-61
SLIDE 61

# file b.py from a import A class B(A): pass # file a.py class A: def use_some_b(self, b: B) -> None: pass $ mypy --strict a.py b.py a.py:6: error: Name 'B' is not defined

slide-62
SLIDE 62

# file b.py from a import A class B(A): pass # file a.py from b import B class A: def use_some_b(self, b: B) -> None: pass

slide-63
SLIDE 63

# file b.py from a import A class B(A): pass # file a.py from b import B class A: def use_some_b(self, b: B) -> None: pass $ mypy --strict a.py b.py $

slide-64
SLIDE 64

# file b.py from a import A class B(A): pass # file a.py from b import B class A: def use_some_b(self, b: B) -> None: pass $ mypy --strict a.py b.py $ $ python a.py File "b.py", line 2, in <module> from a import A File "a.py", line 2, in <module> from b import B File "b.py", line 2, in <module> from a import A ImportError: cannot import name 'A' from 'a'

slide-65
SLIDE 65

# file b.py from a import A class B(A): pass # file a.py from b import B class A: def use_some_b(self, b: B) -> None: pass Ouch, import cycle ! $ mypy --strict a.py b.py $ $ python a.py File "b.py", line 2, in <module> from a import A File "a.py", line 2, in <module> from b import B File "b.py", line 2, in <module> from a import A ImportError: cannot import name 'A' from 'a'

slide-66
SLIDE 66

# file b.py from a import A class B(A): pass # file a.py # we do not import B and use quotes # like in forward references class A: def use_some_b(self, b: "B") -> None: pass

slide-67
SLIDE 67

# file b.py from a import A class B(A): pass # file a.py # we do not import B and use quotes # like in forward references class A: def use_some_b(self, b: "B") -> None: pass $ python a.py $ $ mypy --strict a.py b.py a.py:6: error: Name 'B' is not defined

slide-68
SLIDE 68

# file a.py from typing import TYPE_CHECKING if TYPE_CHECKING: from b import B class A: def use_some_b(self, b: "B") -> None: pass # file b.py from a import A class B(A): pass

slide-69
SLIDE 69

# file a.py from typing import TYPE_CHECKING if TYPE_CHECKING: from b import B class A: def use_some_b(self, b: "B") -> None: pass # file b.py from a import A class B(A): pass $ python a.py $ $ mypy --strict a.py b.py $