3.8. Typing Callable¶
Before Python 3.9 you need
from typing import List, Set, Tuple, Dict
Since Python 3.9: PEP 585 -- Type Hinting Generics In Standard Collections
3.8.1. Return¶
>>> def say_hello() -> str:
... return 'My name... José Jiménez'
3.8.2. Parameters¶
Required:
>>> def add(a: int, b: int):
... return a + b
Optional:
>>> def add(a: int = 1, b: int = 1):
... return a + b
3.8.3. Union¶
Since Python 3.10: PEP 604 -- Allow writing union types as X | Y
>>> def add(a: int | float, b: int | float) -> int | float:
... return a + b
3.8.4. Optional¶
>>> def find(character, text) -> int | None:
... position = text.find(character)
... if position == -1:
... return None
... return position
3.8.5. Alias¶
Since Python 3.10: PEP 604 -- Allow writing union types as X | Y
>>> number = int | float
>>>
>>> def add(a: number, b: number) -> number:
... return a + b
3.8.6. Exception¶
>>> def div(a: float, b: float) -> float:
... if b == 0:
... raise ZeroDivisionError
... return a / b
>>> def div(a: float, b: float) -> float | Exception:
... if b == 0:
... raise ZeroDivisionError
... return a / b
>>> def div(a: float, b: float) -> float | ZeroDivisionError:
... if b == 0:
... raise ZeroDivisionError
... return a / b
3.8.7. Literal¶
Since Python 3.8: PEP 586 -- Literal Types
Literal de-duplicates parameters
Equality comparisons of Literal objects are not order dependent
https://docs.python.org/3/library/typing.html#typing.Literal
SetUp:
>>> from typing import Literal
Definition:
>>> def open(filename: str, mode: Literal['r','w','a']) -> None:
... pass
Usage:
>>> open('data.csv', mode='w') # mypy: OK
>>> open('data.csv', mode='r') # mypy: OK
>>> open('data.csv', mode='a') # mypy: OK
>>> open('data.csv', mode='x') # mypy: ERROR
3.8.8. Callable¶
Function is Callable
Callable
Callable[[int, int], float]
is a function of(int, int) -> float
There is no syntax to indicate optional or keyword arguments
https://docs.python.org/3/library/typing.html#typing.Callable
SetUp:
>>> from typing import Callable
Define:
>>> def add(a: int, b: int) -> int:
... return a + b
>>>
>>> x: Callable = add
>>> x: Callable[..., int] = add
>>> x: Callable[[int,int], int] = add
Parameter:
>>> def run(func: Callable[[int, int], float]):
... ...
3.8.9. Iterator¶
All Generators are Iterators
Generator[yield_type, send_type, return_type]
Iterator[yield_type]
SetUp:
>>> from typing import Iterator, Generator
Generator type annotations:
>>> def fib(n: int) -> Generator[int, None, None]:
... a, b = 0, 1
... while a < n:
... yield a
... a, b = b, a + b
All Generators are Iterators so you can write:
>>> def fib(n: int) -> Iterator[int]:
... a, b = 0, 1
... while a < n:
... yield a
... a, b = b, a + b
3.8.10. Annotations¶
>>> def add(a: int, b: int) -> int:
... return a + b
>>>
>>>
>>> add.__annotations__
{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
3.8.11. Errors¶
Python will execute without even warning
Your IDE and
mypy
et. al. will yield errors
>>> def add(a: int, b: int) -> int:
... return a + b
>>>
>>>
>>> add('Mark', 'Watney')
'MarkWatney'
3.8.12. Good Engineering Practices¶
>>> def add(a: int | float,
... b: int | float,
... ) -> int | float:
... return a + b
3.8.13. Literal String¶
Since Python 3.11: PEP 675 -- Arbitrary Literal String Type
SetUp:
>>> from typing import LiteralString
Example:
>>> def execute(sql: LiteralString):
... ...
>>>
>>> username = 'mwatney'
>>>
>>>
>>> execute('SELECT * FROM users WHERE login="mwatney"') # ok
>>> execute('SELECT * FROM users WHERE login=' + username) # ok
>>> execute('SELECT * FROM users WHERE login=%s' % username) # error
>>> execute('SELECT * FROM users WHERE login=%(login)s' % {'login': username}) # error
>>> execute('SELECT * FROM users WHERE login={}'.format(username)) # error
>>> execute('SELECT * FROM users WHERE login={0}'.format(username)) # error
>>> execute('SELECT * FROM users WHERE login={login}'.format(login=username)) # error
>>> execute(f'SELECT * FROM users WHERE login={username}') # error
3.8.14. Use Case - 0x01¶
>>> def valid_email(email: str) -> str | Exception:
... if '@' in email:
... return email
... else:
... raise ValueError('Invalid Email')
>>>
>>>
>>> valid_email('mwatney@nasa.gov')
'mwatney@nasa.gov'
>>>
>>> valid_email('mwatney_at_nasa.gov')
Traceback (most recent call last):
ValueError: Invalid Email
3.8.15. Use Case - 0x02¶
>>> def find(text: str, what: str) -> int | None:
... position = text.find(what)
... if position == -1:
... return None
... else:
... return position
>>>
>>>
>>> find('Python', 'x')
>>> find('Python', 'o')
4
3.8.16. Use Case - 0x03¶
>>> from urllib.request import urlopen
>>> from typing import Any
>>>
>>>
>>> def fetch(url: str,
... on_success: Callable[[str], Any] = lambda result: ...,
... on_error: Callable[[Exception], Any] = lambda error: ...,
... ) -> None:
... try:
... result: str = urlopen(url).read().decode('utf-8')
... except Exception as err:
... on_error(err)
... else:
... on_success(result)
>>> def handle_result(result: str) -> None:
... print('Success', result)
>>>
>>> def handle_error(error: Exception) -> None:
... print('Error', error)
>>>
>>>
>>> fetch(
... url='https://python3.info',
... on_success=handle_result,
... on_error=handle_error,
... )
>>> fetch(
... url='https://python3.info',
... on_success=lambda result: print(result),
... on_error=lambda error: print(error),
... )
3.8.17. Use Case - 0x04¶
>>> import json
>>> from datetime import date
>>> from typing import Any
>>> data = {'firstname': 'Mark', 'lastname': 'Watney'}
>>> json.dumps(data)
'{"firstname": "Mark", "lastname": "Watney"}'
>>> data = {'firstname': 'Mark', 'lastname': 'Watney', 'birthday': date(1969, 7, 21)}
>>> json.dumps(data)
Traceback (most recent call last):
TypeError: Object of type date is not JSON serializable
>>> def encoder(obj: Any) -> str:
... if isinstance(obj, date):
... return obj.isoformat()
...
>>>
>>> json.dumps(data, default=encoder)
'{"firstname": "Mark", "lastname": "Watney", "birthday": "1969-07-21"}'
3.8.18. Further Reading¶
More information in Type Annotations
More information in CI/CD Type Checking