Saltar al contenido

Introducción a los Tipos de Python

Python tiene soporte para "type hints" opcionales (también llamados "anotaciones de tipo").

Estos "type hints" o anotaciones son una sintaxis especial que permite declarar el tipo de una variable.

Al declarar tipos para tus variables, los editores y las herramientas pueden darte mejor soporte.

Esto es solo un tutorial rápido / repaso sobre los type hints de Python. Cubre solo lo mínimo necesario para usarlos con FastAPI... que en realidad es muy poco.

FastAPI está basado por completo en estos type hints, le dan muchas ventajas y beneficios.

Pero incluso si nunca usas FastAPI, te beneficiarías de aprender un poco sobre ellos.

Nota

Si eres un experto en Python, y ya sabes todo sobre los type hints, salta al siguiente capítulo.

Motivación

Empecemos con un ejemplo simple:

def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

Llamar a este programa produce:

John Doe

La función hace lo siguiente:

  • Toma un first_name y un last_name.
  • Convierte la primera letra de cada uno a mayúscula con title().
  • Los concatena con un espacio en el medio.
def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

Editarlo

Es un programa muy simple.

Pero ahora imagina que lo estabas escribiendo desde cero.

En algún momento empiezas a definir la función, y tienes los parámetros listos...

Pero luego tienes que llamar "a ese método que convierte la primera letra a mayúscula".

¿Era upper? ¿Era uppercase? ¿first_uppercase? ¿capitalize?

Entonces, intentas con el viejo amigo de los programadores, el autocompletado del editor.

Escribes el primer parámetro de la función, first_name, luego un punto (.) y luego presionas Ctrl+Space para activar el autocompletado.

Pero, lamentablemente, no obtienes nada útil:

Añadir tipos

Modifiquemos una sola línea de la versión anterior.

Cambiaremos exactamente este fragmento, los parámetros de la función, de:

    first_name, last_name

a:

    first_name: str, last_name: str

Eso es todo.

Esos son los "type hints":

def get_full_name(first_name: str, last_name: str):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

Eso no es lo mismo que declarar valores por defecto como sería con:

    first_name="john", last_name="doe"

Es una cosa diferente.

Estamos usando dos puntos (:), no iguales (=).

Y añadir type hints normalmente no cambia lo que pasa comparado con lo que pasaría sin ellos.

Pero ahora, imagina que estás de nuevo en medio de la creación de esa función, pero con type hints.

En el mismo punto, intentas activar el autocompletado con Ctrl+Space y ves:

Con eso, puedes desplazarte, viendo las opciones, hasta que encuentres la que "te suene":

Más motivación

Revisa esta función, ya tiene type hints:

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + age
    return name_with_age

Como el editor conoce los tipos de las variables, no solo obtienes autocompletado, también obtienes verificación de errores:

Ahora sabes que tienes que arreglarlo, convertir age a un string con str(age):

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + str(age)
    return name_with_age

Declarar tipos

Acabas de ver el lugar principal para declarar type hints. Como parámetros de funciones.

Este es también el lugar principal donde los usarías con FastAPI.

Tipos simples

Puedes declarar todos los tipos estándar de Python, no solo str.

Puedes usar, por ejemplo:

  • int
  • float
  • bool
  • bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
    return item_a, item_b, item_c, item_d, item_e

Módulo typing

Para algunos casos de uso adicionales, podrías necesitar importar algunas cosas del módulo typing de la biblioteca estándar, por ejemplo cuando quieres declarar que algo tiene "cualquier tipo", puedes usar Any de typing:

from typing import Any


def some_function(data: Any):
    print(data)

Tipos genéricos

Algunos tipos pueden tomar "parámetros de tipo" entre corchetes, para definir sus tipos internos, por ejemplo una "lista de strings" se declararía list[str].

Estos tipos que pueden tomar parámetros de tipo se llaman Tipos genéricos o Genéricos.

Puedes usar los mismos tipos integrados como genéricos (con corchetes y tipos dentro):

  • list
  • tuple
  • set
  • dict

List

Por ejemplo, definamos una variable como una list de str.

Declara la variable, con la misma sintaxis de dos puntos (:).

Como tipo, pon list.

Como la lista es un tipo que contiene algunos tipos internos, los pones entre corchetes:

def process_items(items: list[str]):
    for item in items:
        print(item)

Nota

Esos tipos internos entre los corchetes se llaman "parámetros de tipo".

En este caso, str es el parámetro de tipo pasado a list.

Eso significa: "la variable items es una list, y cada uno de los elementos en esta lista es un str".

Al hacer eso, tu editor puede proporcionar soporte incluso mientras procesa los elementos de la lista:

Sin tipos, eso es casi imposible de lograr.

Observa que la variable item es uno de los elementos en la lista items.

Y aún así, el editor sabe que es un str, y proporciona soporte para eso.

Tuple y Set

Harías lo mismo para declarar tuples y sets:

def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
    return items_t, items_s

Esto significa:

  • La variable items_t es un tuple con 3 elementos, un int, otro int, y un str.
  • La variable items_s es un set, y cada uno de sus elementos es de tipo bytes.

Dict

Para definir un dict, pasas 2 parámetros de tipo, separados por comas.

El primer parámetro de tipo es para las claves del dict.

El segundo parámetro de tipo es para los valores del dict:

def process_items(prices: dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)

Esto significa:

  • The variable prices is a dict:
    • Las claves de este dict son de tipo str (digamos, el nombre de cada elemento).
    • Los valores de este dict son de tipo float (digamos, el precio de cada elemento).

Union

Puedes declarar que una variable puede ser cualquiera de varios tipos, por ejemplo, un int o un str.

Para definirlo usas la barra vertical (|) para separar ambos tipos.

Esto se llama "union" (unión), porque la variable puede ser cualquier cosa en la unión de esos dos conjuntos de tipos.

def process_item(item: int | str):
    print(item)

Esto significa que item podría ser un int o un str.

Posiblemente None

Puedes declarar que un valor podría tener un tipo, como str, pero que también podría ser None.

def say_hi(name: str | None = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")

Usar str | None en lugar de solo str le permitirá al editor ayudarte a detectar errores donde podrías estar asumiendo que un valor siempre es un str, cuando en realidad también podría ser None.

Clases como tipos

También puedes declarar una clase como el tipo de una variable.

Digamos que tienes una clase Person, con un nombre:

class Person:
    def __init__(self, name: str):
        self.name = name


def get_person_name(one_person: Person):
    return one_person.name

Luego puedes declarar una variable como de tipo Person:

class Person:
    def __init__(self, name: str):
        self.name = name


def get_person_name(one_person: Person):
    return one_person.name

Y luego, de nuevo, obtienes todo el soporte del editor:

Ten en cuenta que esto significa "one_person es una instancia de la clase Person".

No significa "one_person es la clase llamada Person".

Modelos de Pydantic

Pydantic es una biblioteca de Python para realizar validación de datos.

Declaras la "forma" de los datos como clases con atributos.

Y cada atributo tiene un tipo.

Luego creas una instancia de esa clase con algunos valores y validará los valores, los convertirá al tipo apropiado (si es el caso) y te dará un objeto con todos los datos.

Y obtienes todo el soporte del editor con ese objeto resultante.

Un ejemplo de la documentación oficial de Pydantic:

from datetime import datetime

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: datetime | None = None
    friends: list[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123

Nota

Para aprender más sobre Pydantic, consulta su documentación.

FastAPI está basado por completo en Pydantic.

Verás mucho más de todo esto en práctica en el Tutorial - Guía de Usuario.

Type Hints con Anotaciones de Metadatos

Python también tiene una característica que permite poner metadatos adicionales en estos type hints usando Annotated.

Puedes importar Annotated desde typing.

from typing import Annotated


def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
    return f"Hello {name}"

Python por sí mismo no hace nada con este Annotated. Y para los editores y otras herramientas, el tipo sigue siendo str.

Pero puedes usar este espacio en Annotated para proporcionar a FastAPI metadatos adicionales sobre cómo quieres que se comporte tu aplicación.

Lo importante a recordar es que el primer parámetro de tipo que pasas a Annotated es el tipo real. El resto, es solo metadatos para otras herramientas.

Por ahora, solo necesitas saber que Annotated existe, y que es Python estándar. 😎

Más adelante verás lo poderoso que puede ser.

Consejo

El hecho de que esto sea Python estándar significa que seguirás obteniendo la mejor experiencia de desarrollo posible en tu editor, con las herramientas que usas para analizar y refactorizar tu código, etc. ✨

Y también que tu código será muy compatible con muchas otras herramientas y bibliotecas de Python. 🚀

Type hints en FastAPI

FastAPI aprovecha estos type hints para hacer varias cosas.

Con FastAPI declaras parámetros con type hints y obtienes:

  • Editor support.
  • Type checks.

...y FastAPI usa las mismas declaraciones para:

  • Definir requisitos: a partir de parámetros de ruta de la petición, parámetros de consulta, headers, bodies, dependencias, etc.
  • Convertir datos: de la petición al tipo requerido.
  • Validate data: coming from each request:
    • Generar errores automáticos devueltos al cliente cuando los datos son inválidos.
  • Document the API using OpenAPI:
    • el cual es luego usado por las interfaces de usuario de documentación interactiva automática.

Esto puede sonar abstracto. No te preocupes. Verás todo esto en acción en el Tutorial - Guía de Usuario.

Lo importante es que al usar tipos estándar de Python, en un solo lugar (en lugar de añadir más clases, decoradores, etc), FastAPI hará gran parte del trabajo por ti.

Nota

Si ya pasaste por todo el tutorial y volviste para ver más sobre tipos, un buen recurso es la "cheat sheet" de mypy.