Anotaciones de tipo en Python y por qué deberías usarlas
Introducción
En equipos solemos heredar código que no escribimos nosotros. Aunque tengamos todo el conocimiento técnico del mundo, toma tiempo entender la lógica detrás de una función ajena; o incluso la nuestra, escrita hace unas semanas o meses atrás. Una de las primeras preguntas que nos hacemos al leer una función es simple, pero fundamental: ¿qué tipo de dato espera recibir, y qué tipo de dato devuelve?
Lenguajes de tipado estricto responden esa pregunta antes de que el código corra. En Rust, por ejemplo, cada variable, argumento y valor de retorno tiene un tipo declarado. Si intentas pasar un `String` donde se espera un `i32`, el compilador lo verifica y se detiene antes de ejecutar el código. En la práctica, esta seguridad de tipos tiene como costo la verbosidad y el mayor tiempo de desarrollo.
Python, en cambio, es de tipado dinámico: no necesita saber el tipo de una variable de antemano porque todo se resuelve en runtime, es decir, cuando el código ya está corriendo y llega a esa línea. Esa flexibilidad es justamente lo que hace a Python (y a lenguajes como R) tan rápidos y simples de escribir, y por eso se usan tanto para scripting. Pero esto también tiene un costo: los errores de tipo no aparecen al escribir el código, sino al ejecutarlo; a veces mucho después, como en producción.
Python no busca convertirse en Rust, pero sí está haciendo un esfuerzo creciente por adoptar la visibilidad de tipos. Desde Python 3.5, con el [PEP 484](https://peps.python.org/pep-0484/), existen las anotaciones de tipo o type hints: una forma de declarar el tipo esperado de variables, argumentos de funciones y valores de retorno. Si bien las anotaciones son completamente opcionales, en este blog te contamos por qué son altamente recomendadas.
Una pequeña introducción a las anotaciones de tipo en Python
La sintaxis es simple: añade dos puntos (`:`) después del nombre de la variable o argumento, seguido del tipo de dato esperado. Por ejemplo, en la declaración de una variable:

Se pueden utilizar para anotar los argumentos de una función:
Para anotar el valor de retorno, se usa `->` después de los paréntesis:

También se pueden anotar uniones de tipos, cuando un argumento puede aceptar varios tipos:

Y también se puede anotar el contenido esperado de una colección, para responder a una colección de qué:

También se pueden anotar clases como tipos:

Ventajas de utilizar anotaciones de tipo
1. Comunicación indirecta con el equipo
¿Por qué utilizar las anotaciones de tipo? Para ilustrar su importancia, volvamos a uno de los ejemplos anteriores:

A simple vista, podemos intuir que `nombre` debe ser un `str`, y `edad` debería ser un `int`.
Pero, ¿qué pasa cuando la función no recibe los tipos esperados? Aparece un `TypeError` al intentar correr el código, pues la operación “mayor o igual” no se ha configurado entre cadenas y enteros.
Si dejamos de asumir que siempre se pasarán los tipos esperados, podemos capturar el error en runtime con una cláusula `try/except`:

Las cláusulas `try/except` son muy útiles porque hay errores que solo se pueden anticipar en runtime, como datos sobre los que no tenemos mucho control. En nuestro código, el error deja de ser un `TypeError` genérico, y se convierte en un mensaje que señala el error claramente. Y, si queremos ser más exhaustivos, podemos agregar un `except AttributeError` si el usuario inserta un tipo diferente a un `str` para el argumento `nombre`.
Pero fijémonos en la lógica detrás de esta solución: escribimos código adicional asumiendo que el usuario de la función se va a equivocar. Las anotaciones de tipo invierten esa lógica, porque comunican de antemano qué se necesita, en vez de reaccionar al error una vez que ya ocurrió. La comunicación en espacios colaborativos es muy importante porque, como se mencionó al inicio, solemos pasar buena parte del desarrollo leyendo código que escribieron otras personas, o que escribimos nosotros un tiempo atrás.

Las anotaciones no reemplazan a los bloques `try/except`, pero tienen la capacidad de resolver el problema en el origen de una manera más efectiva y eficiente. En específico, cambian el foco de reaccionar al error a prevenirlo comunicando la expectativa desde la firma de la función.
2. Comunicación directa con tu editor de código
Nuevamente, volvamos a uno de los ejemplos de la primera sección:

¿Qué era usuario? Tal vez nosotros recordemos que `usuario` es una instancia de la clase `Usuario` definida anteriormente; y no una lista, diccionario o cualquier otro tipo. Sin embargo, sin la anotación, el editor no tiene forma de saberlo. Por esa razón, no sabe qué métodos puede tener. Solo le queda confiar en que aquello que se le pasará como `usuario` tendrá la propiedad `activo`.
Los editores de código, como Visual Studio Code, tienen un feature llamado autocompletado, que sugiere métodos o atributos a medida que escribes. Para funcionar, necesita saber de qué tipo es cada variable. Por lo general tiene la capacidad de inferir los tipos, por ejemplo, al crear variables directamente; pero no puede inferir los tipos de los argumentos de las funciones. En nuestra función, el editor anota `usuario` como `Any`, un tipo especial que significa "podría ser cualquier cosa", por lo que no puede sugerirte métodos específicos para tu variable. Sabes que tu editor está "confiando" cuando los métodos aparecen en blanco, no en otro color.
Con la anotación, ese conocimiento queda escrito en la firma de la función:

Aquí `usuario: Usuario` le dice a tu editor exactamente qué atributos y métodos tiene el objeto. `.nombre`, `.activo`, y te los sugiere al escribir `usuario`. También los subraya en rojo cuando se escribe incorrectamente, previniendo typos.
Esto no es una propiedad exclusiva de objetos definidos por el usuario. Aplica también para `str`, `list`, `dict` y a clases de librerías externas, como `df: pd.DataFrame` en la sección anterior.
Sin embargo, un `DataFrame` es un caso particular, porque no comunica qué columnas tiene ni cuántas filas espera. Esta función es un buen ejemplo:

La anotación `df: pd.DataFrame` asegura que `df` tiene `.loc[]` y `.columns`, pero no comunica que la función espera columnas llamadas exactamente `"ingresos"` y `"costos"`. Si se le pasa un DataFrame con columnas diferentes, o con mayúsculas, el editor no muestra ninguna advertencia.
Para cerrar esa brecha existen herramientas más expresivas, como un `Dataclass` , un `Protocol` (que define los métodos que una clase debe implementar) o los modelos de `Pydantic`, que se verán a continuación.
3. Pseudo seguridad de tipos
Hemos mencionado que Python es un lenguaje de tipado dinámico, porque lo relacionado a tipos correctos se resuelve en runtime, es decir, cuando el código ya está corriendo y llega a esa línea. Esto le da a Python su flexibilidad característica.

Sin embargo, se puede simular seguridad de tipos con herramientas adicionales que aprovechan las anotaciones de tipo, volviéndolas aún más útiles. Hay dos caminos principales.
Type checkers estáticos. Herramientas como `mypy` o la extensión de Python en VS Code tienen la capacidad de leer tus anotaciones sin ejecutar el código, y te avisan de inconsistencias antes de correr el código. Esto es lo más cercano a lo que hace el compilador de Rust. Corriendo `mypy` sobre el ejemplo anterior:

Cabe mencionar que la mayoría de editores no tienen activada la verificación de tipos. Pylance, por ejemplo, tiene modos `off`, `basic`, `standard` y `strict`. Para activarla, se puede modificar el `settings.json` con la siguiente línea.

Validación en runtime, con Pydantic. A diferencia de un type checker, que revisa tu código antes de correr, [Pydantic](https://docs.pydantic.dev/) usa tus anotaciones como reglas que se validan mientras el código se ejecuta. Pero antes, veamos qué problema estamos intentando resolver.
Podríamos representar una configuración simplemente con un diccionario:

Esto es flexible, pero también puede volverse difícil de mantener. Aunque se añadan anotaciones, los diccionarios siguen siendo mutables y muy susceptibles a typos, pues no restringen qué claves pueden existir. Además, tenemos que recordar constantemente qué claves existen y qué tipo de dato espera cada una.
Los `dataclasses` solucionan buena parte de este problema al permitir definir una estructura explícita:

Nuestro editor reconoce los campos esperados por `Config`, cambiando el foco de claves (`config["factor"]`) a atributos (`config.factor`).
Otra manera de simular seguridad de tipos con anotaciones es creando un objeto `Protocol` y utilizarlo como anotación de tipo, por ejemplo, para el argumento de una función. Este objeto no se puede instanciar o heredar, pero permite especificar los métodos y atributos que una clase debe implementar.
Sin embargo, las anotaciones de un objeto no se validan en runtime, así que no restringen el tipo de dato esperado.
Aquí es donde entra Pydantic. La sintaxis es muy parecida a una `dataclass`, pero las anotaciones se convierten en reglas de validación:

Pydantic comprueba los datos en el momento de crear el objeto en runtime y, cuando es posible, también puede convertir valores compatibles.
Si queremos aprovechar más nuestras anotaciones, Pydantic nos permite convertirlas en reglas estrictas a cumplir durante la ejecución, similar al estilo de Rust. Esto es especialmente útil cuando los datos vienen de fuentes externas, como APIs, archivos de configuración o formularios.
De hecho, este patrón es fundamental en librerías como FastAPI, que utiliza Pydantic para validar automáticamente los datos recibidos, convertirlos cuando corresponde y generar documentación a partir de los mismos modelos.
Las anotaciones, entonces, tienen el potencial de convertirse en un contrato que los datos deben cumplir en runtime.
Conclusiones y aprendizajes
Leer código ajeno o antiguo rara vez es solo un problema de lógica. Gran parte del esfuerzo viene de la ambigüedad: no saber qué tipo de dato espera una función y qué tipo de dato devuelve. Las anotaciones de tipo resuelven eso con la comunicación en dos frentes: comunicación indirecta con tu equipo, terceros, o tú mismo dentro de unos meses; y comunicación directa con tu editor, que deja de confiar y empieza a poder sugerir métodos, detectar errores de tipeo y subrayar problemas antes de correr el código. Las anotaciones también tienen la capacidad de convertirse en algo parecido a un contrato de datos si se activa la verificación de tipos (por ejemplo, con mypy) o se utilizan librerías externas que simulan seguridad de tipos, como `Pydantic`. Aunque son opcionales, adoptarlos es altamente recomendado por estas razones.
Recordemos que la legibilidad no es solo una buena práctica o un tema estético, es velocidad colectiva: cuánto tarda un equipo en entender, confiar y modificar código. Las anotaciones de tipo son una de las formas más baratas de ganar esa velocidad.
Referencias
[Fast API](https://fastapi.tiangolo.com/python-types/)