Previous articles covered the use of dataclass, pydantic and orjson and how to use typing; this article explains best practices:
Besides helping to understand the code better, type checking makes it easier to detect errors quickly and automatically using, for example, mypy. Type checking was added to Python in version 3.5.
This article is based on the article in English: Improving Your Python Projects With Type Hints (broken link)
Typing syntax
Python supports type annotations for the basic variables supported by the language: str, int, float, bool and None. It comes built into the typing library.
Variable annotation
You can indicate the type of a variable by using the colon ":". For example:
from typing import List
nombre: str = 'Pedro'
edad: int = 24
altura_mts: float = 1.7
colegas: List[str] = ['Jane', 'John']
The type and the value that the variable will have are defined.
Annotation in functions
In the case of functions, you can add annotations to the argument variables, as well as to the function's return value.
from typing import Dict, List
import math
def calc_area_circulo(radio: float) -> float:
return math.pi*(radio**2)
def send_email(
subject: str,
body: str,
recipients: List[str],
cache: Dict[str, str]
) -> bool:
# ....
# Devuelve True o False el envío de un correo
return True
The first function takes the radius as a float argument and returns the area of the circle, which is also a float.
The following example defines a function that receives subject (string), body (string), recipients (list of strings) and cache (a dictionary with string keys and string values), and the function returns a boolean.
Using special types
There is a series of types that complement the basic ones:
- Any:
- Literal:
- Union
- TypeDict
- NoReturn
- Final
Any
A variable of type Any is compatible with all the basic types.
from typing import Any
resultado: Any = "Procesado"
resultado = 10
estatus: str = "Pendiente"
estatus = resultado
In the first case the variable resultado of type Any is defined and assigned a string, then it is assigned an integer, which is not a problem because the Any type supports it. Then the variable estatus of type string is defined and assigned a string, and then resultado is assigned to the variable estatus without problems; this is not a problem either because resultado is of type any.
Literal
The literal type is used to indicate that a variable has a value equal to one of the indicated values (types).
from typing import Literal
GENERO = Literal["hombre", "mujer", "no especificado"]
def crear_usuario(
nombre: str,
apellido: str,
genero: GENERO,
) -> Dict[str, str]:
return {
"nombre": nombre,
"apellido": apellido,
"genero": genero
}
crear_usuario("John", "Doe", "hombre")
{'nombre': 'John', 'apellido': 'Doe', 'genero': 'hombre'}
A list is defined for the gender of literal type, and the value is one of those defined (hombre, mujer, no especificado).
Union
Some variables sometimes need two basic types.
from typing import Union
def get_temperatura() -> Union[int, float]:
return 20.8 # funciona también con 20.
A function is defined that returns an integer or a float; this is achieved using Union.
TypedDict
TypedDict lets you define a dictionary data type.
from typing import TypedDict, Union
class Card(TypedDict):
rank: Union[str, int]
suit: str
# The Card class now has behaviours of both TypedDict and dict classes
# Card can be used to annotate a variable
ace_of_spade: Card = {'rank': 'A', 'suit': '♤'}
# or can be instantiated
ace_of_spade = Card(rank='A', suit='♤')
print(ace_of_spade)
{'rank': 'A', 'suit': '♤'}
NoReturn
NoReturn is similar to void in other programming languages; in my case I used to use None when a function returns nothing.
from typing import NoReturn
def hola1() -> None:
print("Hola Mundo!")
def hola2() -> NoReturn:
print("Hola Mundo!")
Two hola functions are defined, the first as None and the second as NoReturn, which is how a function that does not return a value should be defined.
Final
This data type is defined so that a value is not redefined.
from typing import Final
MIN_NOMBRE_LONG: Final = 2
# mypy reporta error por que se le está asignando un nuevo valor a la
# variable
MIN_NOMBRE_LONG += 1
class Validador(object):
MIN_NOMBRE_LONG: Final[int] = 4
class UserValidador(Validador):
# El interprete marca una bandera en esta línea
MIN_NOMBRE_LONG = 3
Using type checkers
One of the most widely used type checkers is mypy. It can be installed using pip:
pip install mypy
To use it, simply call mypy and pass it the file to check.
The code to validate is shown below:
#!/usr/bin/env python3
from typing import Literal, Dict
GENERO = Literal["hombre", "mujer", "no especificado"]
def crear_usuario(
nombre: str,
apellido: str,
genero: GENERO,
) -> Dict[str, str]:
return {
"nombre": nombre,
"apellido": apellido,
"genero": genero
}
crear_usuario("John", "Doe", "hombre")
crear_usuario("Jane", "Doe", "mujer")
crear_usuario("John", "Doe", "x")
This code will return an error when trying to create a user with gender x.
mypy pruebas_typing.py
content/code/pruebas_typing.py:23: error: Argument 3 to "crear_usuario" has incompatible type "Literal['x']"; expected "Union[Literal['hombre'], Literal['mujer'], Literal['no especificado']]"
Found 1 error in 1 file (checked 1 source file)
This is the error message returned when trying to set the value x as the gender.
Type notation and checking make it easier to validate that a variable always has a data type and does not change. It happened to me not long ago: I wrote a function that returned a list, and some time later a developer changed the list to a dictionary, and the function worked, but when running mypy on it.
Make a donation! If you liked this article you can donate with Bitcoin (BTC) using the digital wallet of your choice to the following address: 17MtNybhdkA9GV3UNS6BTwPcuhjXoPrSzV
Or by scanning the QR code from your wallet:
