Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Pythonic type hints with typing.Protocol

Pythonic type hints with typing.Protocol

Duck typing is essential to idiomatic Python. Since Python 3.8, typing.Protocol allows static type checkers to verify APIs based on duck typing, a.k.a. structural typing. Using it is key to write well annotated Pythonic code.

Luciano Ramalho

May 24, 2024
Tweet

More Decks by Luciano Ramalho

Other Decks in Programming

Transcript

  1. Fluent Python, Second Edition • Covers 3.10, including pattern matching

    • 100+ pages about type hints, with many examples • New coverage of async/await, with examples in asyncio, FastAPI and Curio • OReilly.com: https://bit.ly/FluPy2e 3
  2. The first parameter is the file-like object to be sent…

    To be considered “file-like”, the object supplied by the application must have a read() method that takes an optional size argument. 6 PEP 3333 – Python Web Server Gateway Interface 6
  3. The words "file-like" appear with similar implied meaning in the

    Python 3.12 distribution: • 148 times in the documentation; • 92 times in code comments in .py or .c source files; • 30 times across 21 PEPs: 100, 214, 258, 282, 305, 310, 333, 368, 400, 441, 444, 578, 680, 691, 3116, 3119, 3143, 3145, 3154, 3156, 3333.
  4. 4 What is a type? 1. The four modes of

    typing 2. typing.Protocol examples 3. Conclusion 4. 4 Agenda
  5. There are many definitions of the concept of type in

    the literature. Here we assume that type is a set of values and a set of functions that one can apply to these values. 6 Guido van Rossum, Ivan Levkivskyi in PEP 483—The Theory of Type Hints 6
  6. 12 >>> i = 10**23 >>> i 100000000000000000000000 >>> f

    = float(i) >>> f 1e+23 >>> i == f False >>> from decimal import Decimal >>> Decimal(i) Decimal('100000000000000000000000') >>> Decimal(f) Decimal('99999999999999991611392') >>> i | 2 100000000000000000000002 >>> f | 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for |: 'float' and 'int'
  7. 12 >>> i = 10**23 >>> i 100000000000000000000000 >>> f

    = float(i) >>> f 1e+23 >>> i == f False >>> from decimal import Decimal >>> Decimal(i) Decimal('100000000000000000000000') >>> Decimal(f) Decimal('99999999999999991611392') >>> i | 2 100000000000000000000002 >>> f | 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for |: 'float' and 'int'
  8. 12 >>> i = 10**23 >>> i 100000000000000000000000 >>> f

    = float(i) >>> f 1e+23 >>> i == f False >>> from decimal import Decimal >>> Decimal(i) Decimal('100000000000000000000000') >>> Decimal(f) Decimal('99999999999999991611392') >>> i | 2 100000000000000000000002 >>> f | 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for |: 'float' and 'int'
  9. The “set of values” definition is not useful: Python does

    not provide ways to specify types as sets of values, except for Enum. We have very small sets (None, bool) or very large ones (int, str…). 9
  10. Python type hints cannot define a Quantity type as the

    set of integers 0 < n 1000 ≤ or… AirportCode as the set of all 17576 three-letter ASCII strings like “FLR”, ”LAX”, “BER” etc. 10
  11. In practice, it’s more useful to think that int is

    a subtype of float because it implements the same interface, and adds extra methods —not because int is a subset of float* 11 * which it definitely isn’t
  12. 12 >>> i = 10**23 >>> i 100000000000000000000000 >>> f

    = float(i) >>> f 1e+23 >>> i == f False >>> from decimal import Decimal >>> Decimal(i) Decimal('100000000000000000000000') >>> Decimal(f) Decimal('99999999999999991611392') >>> i | 2 100000000000000000000002 >>> f | 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for |: 'float' and 'int'
  13. There are many definitions of the concept of type in

    the literature. Here we assume that type is a set of values and a set of functions that one can apply to these values. 14 14 Guido van Rossum, Ivan Levkivskyi in PEP 483—The Theory of Type Hints
  14. Every object in Smalltalk, even a lowly integer, has a

    set of messages, a protocol, that defines the explicit communication to which that object can respond. 15 Dan Ingalls (Xerox PARC) in Design Principles Behind Smalltalk —BYTE Magazine, August 1981 15
  15. Don’t check whether it is-a duck: check whether it quacks-like-a

    duck, walks-like-a duck, etc, depending on exactly what subset of duck-like behavior you need... 18 Alex Martelli in comp-lang-python: ”polymorphism (was Re: Type checking in python?)” 2000-07-26 18
  16. 12 >>> def double(x): ... return x * 2 ...

    >>> double(3) 6 >>> double(3.5) 7.0 >>> double(3j+4) (8+6j) >>> from fractions import Fraction >>> double(Fraction(1, 3)) Fraction(2, 3) >>> double('Spam') 'SpamSpam' >>> double([1, 2, 3]) [1, 2, 3, 1, 2, 3] >>>
  17. 12 >>> class Train: ... def __init__(self, cars): ... self.cars

    = cars ... def __iter__(self): ... for i in range(self.cars): ... yield f'car #{i+1}' ... >>> t = Train(4) >>> for car in t: ... print(car) ... car #1 car #2 car #3 car #4
  18. 12 >>> class Train: ... def __init__(self, cars): ... self.cars

    = cars ... def __getitem__(self, i): ... if i < self.cars: ... return f'car #{i+1}' ... raise IndexError ... def __len__(self): ... return self.cars ... >>> t = Train(4) >>> len(t) 4 >>> t[0] 'car #1' >>> for car in t: ... print(car) ... car #1 car #2
  19. Preserve the flexibility of duck typing Let your clients know

    what is the minimal interface expected, regardless of class hierarchies Benefits of using typing.Protocol 26 Support static analysis IDEs and linters can verify that an actual argument satisfies the protocol in the formal parameter Reduce coupling Client classes don’t need to subclass anything; just implement the protocol.
  20. Preserve the flexibility of duck typing Let your clients know

    what is the minimal interface expected, regardless of class hierarchies Benefits of using typing.Protocol 27 Support static analysis IDEs and linters can verify that an actual argument satisfies the protocol in the formal parameter Reduce coupling Client classes don’t need to subclass anything; just implement the protocol.
  21. Preserve the flexibility of duck typing Let your clients know

    what is the minimal interface expected, regardless of class hierarchies Benefits of using typing.Protocol 28 Support static analysis IDEs and linters can verify that an actual argument satisfies the protocol in the formal parameter Reduce coupling Client classes don’t need to subclass anything; just implement the protocol. This also makes testing easier.
  22. Python will remain a dynamically typed language, and the authors

    have no desire to ever make type hints mandatory, even by convention. 32 Guido van Rossum, Jukka Lehtosalo, Łukasz Langa in PEP 484—Type Hints 32
  23. 36

  24. 12 >>> def double(x): ... return x * 2 ...

    >>> double(3) 6 >>> double(3.5) 7.0 >>> double(3j+4) (8+6j) >>> from fractions import Fraction >>> double(Fraction(1, 3)) Fraction(2, 3) >>> double('Spam') 'SpamSpam' >>> double([1, 2, 3]) [1, 2, 3, 1, 2, 3] >>>
  25. Every Python value is of type object. Every Python value

    is of type Any. The object type implements a narrow interface, but Any is assumed to implement the widest possible interface: all possible methods! 13
  26. Fourth take: protocol misuse 45 45 Not OK: Type checker

    assumes that result supports only __mul__, and no other method.
  27. 48

  28. 49

  29. First uses of the SupportsLessThan Protocol Stub files for Python

    3.9 standard library on typeshed in 2020 51 51 builtins: list.sort max min sorted statistics: median_low median_high functools: cmp_to_key bisect: bisect_left bisect_right insort_left insort_right heapq: nlargest nsmallest os.path: commonprefix
  30. Today* there are 120** Protocol definitions on typeshed/stdlib and nearly

    100** on /stubs*** * 2024-05-24 ** not counting network protocols *** for external packages like Pillow, psycopg2, tensorflow, etc.
  31. 54

  32. 59 59 26 Lines of code to implement all the

    documented functionality, with 2 constants, no imports 29 Lines of code for type hints: 7 imports, 4 definitions, and 6 overloaded signatures
  33. max overload: postscript 60 After I contributed SupportsLessThan to typeshed,

    a few things happened: • Edge cases were discovered where SupportsGreaterThan was needed • SupportsLessThan was replaced with SupportsGreaterThan, but this exposed symmetric bugs in cases that previously worked • Both were superseded by SupportsDunderLT and SupportsDunderGT • Almost all of their uses were replaced with a new type—the union of both of them—named SupportsRichComparison ◦ This means most functions that involve comparisons in the standard library now have type hints that accept objects implementing either < or >. No need to implement both. • For details, see: https://github.com/python/typeshed/blob/master/stdlib/_typeshed/__init__.pyi
  34. Support duck typing with type hints The essence of Python’s

    Data Model and standard library Use typing.Protocol to build Pythonic APIs 65 Follow the Interface Segregation Principle Client code should not be forced to depend on methods it does not use Prefer narrow protocols Single method protocols should be the most common. Sometimes, two
  35. Support duck typing with type hints The essence of Python’s

    Data Model and standard library to build Pythonic APIs 66 Follow the Interface Segregation Principle Client code should not be forced to depend on methods it does not use Prefer narrow protocols Single method protocols should be the most common. Sometimes, two Use typing.Protocol
  36. Support duck typing with type hints The essence of Python’s

    Data Model and standard library to build Pythonic APIs 67 Follow the Interface Segregation Principle Client code should not be forced to depend on methods it does not use Prefer narrow protocols Single method protocols should be the most common. Sometimes, two methods. Rarely more. Use typing.Protocol
  37. 69 69 Being optional is not a bug of Python

    type hints. It’s a feature that gives us the power to cope with the inherent complexities, annoyances, flaws, and limitations of static types.
  38. I don’t hesitate to use # type: ignore to avoid

    the limitations of static type checkers when submission to the tool would make the code worse or needlessly complicated. 70 70
  39. I don’t hesitate to use # type: ignore to avoid

    the limitations of static type checkers when submission to the tool would make the code worse or needlessly complicated. 70 70 Me, in Fluent Python Second Edition
  40. I don’t hesitate to use # type: ignore to avoid

    the limitations of static type checkers when submission to the tool would make the code worse or needlessly complicated. 70 70 Me, in Fluent Python Second Edition @[email protected]