Python, a pesar de ser un lenguaje de tipado dinámico, permite el uso de anotaciones de tipos para indicar los tipos de datos esperados en variables, argumentos de funciones y valores de retorno. Aunque no son obligatorias, estas anotaciones contribuyen a mejorar la legibilidad y el mantenimiento del código, además de facilitar la detección de errores potenciales.

Las anotaciones de tipos mejoran la experiencia de desarrollo al ofrecer sugerencias más precisas en los editores de código. Además, algunas librerías como Pydantic aprovechan estas anotaciones para proporcionar características avanzadas, como la validación automática de datos.

Anotación en variables

Puedes anotar las variables simples con su tipo utilizando : seguido del tipo de dato. Los tipos pueden ser cualquiera de los básicos de Python, concretamente str, int, float y bool. Aunque no es común anotar variables, en algunos casos puede ser útil para aclarar el comportamiento del código.

nombre: str = "Juan"
edad: int = 30

Anotación en funciones

Puedes realizar las anotaciones en los parámetros de las funciones de la misma manera que con las variables. Para el valor de retorno utiliza -> seguido del tipo. Es común ver la anotación -> None, que indica que la función no tiene valor de retorno (por ejemplo, el método __init__() de una clase).

def envia_saludo(nombre: str) -> str:
    return f"Hola, {nombre}"
 
 
def imprime_saludo(nombre: str) -> None:
    print(f"Hola, {nombre}")

Tipos compuestos

Para anotar tipos en colecciones como listas, diccionarios o tuplas, utiliza [] para indicar el tipo de los elementos que contiene. En el caso de los diccionarios, debes especificar los tipos tanto de las claves como de los valores, mientras que para las tuplas debes anotar los tipos de cada uno de sus elementos.

def suma_numeros(numeros: list[int]) -> int:
    return sum(numeros)
 
 
def cuenta_ocurrencias(palabras: list[str]) -> dict[str, int]:
    resultado = {}
    for palabra in palabras:
        resultado[palabra] = resultado.get(palabra, 0) + 1
 
    return resultado

Uniones y opcionales

Puedes utilizar el operador | para indicar una unión de tipos, lo que permite especificar que una variable o parámetro puede aceptar múltiples tipos. Por ejemplo, si un parámetro puede ser tanto int como float, puedes indicarlo así:

def cubo(numero: int | float) -> int | float:
    return numero**3

También puedes usar None en combinación con otro tipo para señalar que el valor es opcional. Esto puedes lograrlo con el operador |:

def buscar_elemento(lista: list[int], elemento: int) -> int | None:
    if elemento in lista:
        return lista.index(elemento)
    return None

O bien, utilizando Optional del módulo typing:

from typing import Optional
 
 
def buscar_elemento(lista: list[int], elemento: int) -> Optional[int]:
    if elemento in lista:
        return lista.index(elemento)
    return None

Uso de clases en anotaciones de tipos

Además de los tipos primitivos, también puedes utilizar clases personalizadas en las anotaciones de tipos. Esto es especialmente útil cuando una función espera recibir o retornar una instancia de una clase específica. Esto facilita la comprensión del código, permite una mejor autocompletación en los entornos de desarrollo y mejora la legibilidad.

En el siguiente ejemplo, la función imprime_informacion() toma como argumento un objeto de la clase Persona:

class Persona:
    def __init__(self, nombre: str, edad: int):
        self.nombre = nombre
        self.edad = edad
 
    def saludar(self) -> str:
        return f"Hola, mi nombre es {self.nombre} y tengo {self.edad} años."
 
 
def imprime_informacion(persona: Persona) -> None:
    print(persona.saludar())

Aquí, la anotación de tipo Persona indica claramente que el parámetro persona debe ser una instancia de la clase Persona, mejorando la claridad y proporcionando una verificación estática de tipos con herramientas como mypy.

Clases en colecciones

Es posible combinar clases con colecciones, al igual que con tipos primitivos. Por ejemplo, si tienes una función que devuelve una lista de objetos de la clase Persona, puedes anotarla de la siguiente manera:

def carga_personas() -> list[Persona]:
    datos = []
    # carga de datos...
    return datos

Aquí, la anotación list[Persona] indica que la función retorna una lista que contiene instancias de la clase Persona.

Anotaciones adelantadas

En algunos casos, puede que necesites anotar una clase que aún no ha sido definida, como cuando dos clases dependen mutuamente en sus anotaciones. Para estos casos, puedes usar comillas para indicar el nombre de la clase, retrasando la evaluación de la anotación hasta que la clase sea definida. Esto es conocido como una anotación adelantada:

class A:
    def __init__(self, b: list["B"]):
        self.b = b
 
 
class B:
    def __init__(self, a: "A"):
        self.a = a

En este ejemplo, las clases A y B hacen referencia a sí mismas de manera recíproca, lo que se logra usando comillas alrededor de los nombres de las clases en las anotaciones de tipo.

Validación de anotaciones de tipos

Python no realiza validaciones automáticas de los tipos anotados. Sin embargo, los editores de código como VS Code o PyCharm usan esta información para ayudar a detectar errores en tiempo de desarrollo.

Además, existen herramientas como mypy o pyright que permiten verificar que el código cumple con las anotaciones de tipos.

Por ejemplo, dado el siguiente código:

def suma(a: int, b: int) -> int:
    return a + b
 
 
resultado = suma(10, 12.5)  # float en lugar de un int

Al ejecutar mypy con este código, se generará el siguiente error:

main.py:4: error: Argument 2 to "suma" has incompatible type "float"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

No obstante, si se ejecuta este código directamente con Python, no se lanzará ningún error.