Lección 0.4.2 · 45 min

Tipado: type hints, mypy o ty y Pydantic para configuraciones

Python no comprueba tipos al ejecutar: un precio leído como texto se cuela hasta que algo explota, o peor, hasta que no explota. Las anotaciones documentan qué espera cada función, un comprobador estático encuentra los errores antes de ejecutar y Pydantic valida en la frontera, donde entran los datos y las configuraciones.

02/06
Módulo
0.4 · Python intermedio
Duración
45 min
Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

Lo que Python no comprueba

Python es un lenguaje de tipado dinámico: cada valor sabe de qué tipo es, pero una variable o un argumento pueden recibir cualquier cosa. La función de la lección 0.4.1 que calculaba el importe de una orden funciona con números y también, sin quejarse, con texto. Basta un precio leído de un CSV sin convertir:

IN [01]python
def importe(cantidad: int, precio: float) -> float:
    return cantidad * precio

print(importe(2, 142.22))
print(importe(2, "142.22"))
print(importe.__annotations__)
OUT [01]Un precio que llega como texto
284.44
142.22142.22
{'cantidad': <class 'int'>, 'precio': <class 'float'>, 'return': <class 'float'>}

La segunda llamada no lanza ningún error: 2 * "142.22" repite el texto. El resultado viajará por el programa hasta que algo intente sumarlo a un número, o se escribirá tal cual en un informe. Las anotaciones de tipo (type hints), lo que va después de los dos puntos y de la flecha, están ahí, Python las guarda en __annotations__ y no hace nada más con ellas.

Para qué sirven, entonces:

  • Documentan. precio: float dice más que un comentario y no se queda desactualizado sin que nadie lo note.
  • Las leen las herramientas. El editor completa métodos y marca errores mientras se escribe (lección 0.2.3), y un comprobador estático como mypy o ty revisa el programa entero sin ejecutarlo.
  • Las usan algunas bibliotecas. Las dataclasses de la lección 0.4.1 leen las anotaciones para crear los campos; Pydantic, al final de esta lección, las usa para validar datos de verdad.

Hay, por tanto, dos momentos distintos para encontrar un error de tipo, y conviene tener los dos cubiertos:

Código anotadomypy o tyErrores antesDatos externosPydanticError al cargar
Fuente Mermaid
flowchart LR
  C[Código anotado] --> S[mypy o ty]
  S --> E[Errores antes]
  D[Datos externos] --> P[Pydantic]
  P --> V[Error al cargar]

El comprobador estático mira el código; Pydantic mira los datos que entran (un YAML, la respuesta de un bróker). Ni el primero puede saber qué traerá un fichero ni el segundo revisa las funciones internas.

La sintaxis de las anotaciones

Las anotaciones usan los propios tipos de Python y unos pocos símbolos más. Las que aparecen en código de trading:

AnotaciónSignificaEjemplo
int, float, str, boolUn valor de ese tipocantidad: int
list[float]Lista de floatscierres: list[float]
dict[str, float]Diccionario de texto a floatprecios: dict[str, float]
tuple[float, float]Tupla de dos floats-> tuple[float, float] (realizado, no realizado)
float | NoneUn float o NoneUn precio que puede faltar
Callable[[float], bool]Una función que recibe un float y devuelve un boolUn filtro de precios
type Precio = floatAlias de tipo (Python 3.12+)Nombres con significado

La unión con None es la más útil de todas: obliga a tratar el caso en que el dato no existe. Un diccionario de cierres no tiene por qué tener todos los símbolos, y la función que lo consulta lo dice en su firma:

IN [02]python
type Precio = float

def rentabilidades(cierres: list[Precio]) -> list[float]:
    """Rentabilidades simples entre cierres consecutivos."""
    return [b / a - 1 for a, b in zip(cierres, cierres[1:])]

def ultimo(cierres: dict[str, Precio], simbolo: str) -> Precio | None:
    return cierres.get(simbolo)

cierres = {"RVAR": 164.35, "ORO": 171.44}
print([round(r, 4) for r in rentabilidades([100.0, 101.5, 99.0])])
print(ultimo(cierres, "ORO"), ultimo(cierres, "TECH"))
OUT [02]Funciones anotadas
[0.015, -0.0246]
171.44 None

Con -> Precio | None, el comprobador avisa si el código que llama a ultimo hace cuentas con el resultado sin mirar antes si es None. Es exactamente el error de un sistema que valora una posición cuyo precio no llegó: sin anotación, aparece en producción como TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'.

En Python 3.14 las anotaciones se evalúan de forma diferida (PEP 649): una función puede anotar con una clase que se define más abajo en el mismo fichero sin poner el nombre entre comillas.1Nota 1 · PEP 649 y PEP 749, incluidas en Python 3.14: las anotaciones se guardan sin evaluar y se calculan al pedirlas. Detalles en la documentación de annotationlib.

Valores permitidos y diccionarios con forma: Literal y TypedDict

str admite cualquier texto, y el lado de una orden solo admite dos. Literal enumera los valores válidos. TypedDict describe un diccionario con claves fijas y un tipo por clave, la forma habitual de los mensajes JSON de un bróker. NotRequired marca las claves opcionales, como el límite de una orden de mercado:

IN [03]python
from typing import Literal, NotRequired, TypedDict, get_args

type Lado = Literal["compra", "venta"]

class OrdenDict(TypedDict):
    simbolo: str
    lado: Lado
    cantidad: int
    limite: NotRequired[float]

orden: OrdenDict = {"simbolo": "SINT", "lado": "compra",
                    "cantidad": 100, "limite": 141.5}
mala: OrdenDict = {"simbolo": "SINT", "lado": "comprar",
                   "cantidad": 100}
print(type(orden).__name__)
print(get_args(Lado.__value__))
print(mala["lado"] in get_args(Lado.__value__))
OUT [03]Literal y TypedDict para órdenes
dict
('compra', 'venta')
False

La orden mala se crea sin problema: en tiempo de ejecución, un TypedDict es un diccionario normal y un Literal es un texto normal. Quien lo detecta es el comprobador estático, que marca la línea con "comprar" antes de ejecutar nada. Si el valor viene de fuera (un fichero, una API), hay que validarlo al ejecutar; get_args devuelve los valores del Literal para hacerlo a mano, y Pydantic lo hace solo, como se ve más abajo.

Interfaces sin herencia: Protocol

En la lección 0.4.1, el ejecutor aceptaba cualquier objeto con un método permite(orden). Esa «firma común» se declara con un Protocol: una clase que solo describe métodos. Las clases que los tengan cumplen el protocolo sin heredar de él. Es lo que en Python se llama duck typing («si camina como un pato…»), ahora comprobable:

IN [04]python
from dataclasses import dataclass
from typing import Protocol, runtime_checkable

@runtime_checkable
class ReglaRiesgo(Protocol):
    def permite(self, orden: OrdenDict) -> bool: ...

@dataclass
class MaxCantidad:
    maximo: int

    def permite(self, orden: OrdenDict) -> bool:
        return orden["cantidad"] <= self.maximo

@dataclass
class SimbolosPermitidos:
    simbolos: frozenset[str]

    def permite(self, orden: OrdenDict) -> bool:
        return orden["simbolo"] in self.simbolos

def aprobar(orden: OrdenDict, reglas: list[ReglaRiesgo]) -> bool:
    return all(r.permite(orden) for r in reglas)

reglas: list[ReglaRiesgo] = [MaxCantidad(500),
                             SimbolosPermitidos(frozenset({"SINT"}))]
print(aprobar(orden, reglas))
print(aprobar({"simbolo": "TECH", "lado": "venta",
               "cantidad": 10}, reglas))
print(isinstance(MaxCantidad(1), ReglaRiesgo))
OUT [04]Reglas de riesgo con Protocol
True
False
True

MaxCantidad y SimbolosPermitidos no heredan de ReglaRiesgo ni se conocen entre sí. El comprobador sabe que cumplen el protocolo porque tienen permite con la firma correcta; si alguien escribe una regla con permite(self, orden, precio), el error aparece al comprobar, no la primera vez que esa regla rechace una orden real. @runtime_checkable permite además usar isinstance, que solo mira si el método existe, no su firma.

mypy o ty: comprobar sin ejecutar

Las anotaciones sirven de poco si nadie las comprueba. Las dos herramientas habituales son mypy, la de referencia desde hace años, y ty, de Astral (los autores de uv), escrita en Rust y mucho más rápida. Ninguna ejecuta el programa: leen el código, deducen el tipo de cada expresión y comparan con las anotaciones. Se instalan como dependencias de desarrollo del proyecto (lección 0.2.2):

# mypy como dependencia de desarrollo
uv add --dev mypy
uv run mypy src/

# modo estricto: exige anotar todo y prohíbe Any implícito
uv run mypy --strict src/

# ty, sin instalarlo en el proyecto
uvx ty check

Sobre un fichero con las celdas de esta lección, las dos herramientas señalan la llamada importe(2, "142.22") y el diccionario con "lado": "comprar": indican el fichero, la línea y el tipo esperado frente al recibido, y terminan con código de salida distinto de cero. Ese código de salida es lo que permite usarlas en un hook de Git o en la integración continua: si el comprobador falla, el cambio no entra. (Con Pydantic instalado, mypy marca también el media_rapida="50" de la sección siguiente: conoce los tipos del modelo, aunque Pydantic convertiría el texto al ejecutar. Los datos externos se cargan con model_validate, que recibe un diccionario y no tiene ese problema.)

La configuración vive en pyproject.toml, junto al resto del proyecto:

IN [05]toml
[tool.mypy]
python_version = "3.14"
strict = true

Dos consejos prácticos. Empieza por anotar las fronteras: las funciones públicas de cada módulo, las que reciben datos de fuera y las que devuelven resultados que usa otro código. Dentro de una función, el comprobador deduce casi todo solo. Y no abuses de Any ni de # type: ignore: cada uno apaga la comprobación justo donde la pusiste, que suele ser donde hacía falta.

Pydantic: configuraciones que se validan al cargar

Una estrategia tiene parámetros: símbolos, ventanas de las medias, riesgo por operación, costes. Suelen vivir en un fichero de configuración, y un error en ese fichero es de los más caros: un riesgo_por_operacion: 0.2 en lugar de 0.02 multiplica por diez el tamaño de cada posición y ningún test de las funciones lo detecta.

Pydantic convierte las anotaciones en validación en tiempo de ejecución. Un modelo es una clase que hereda de BaseModel; cada campo lleva su tipo y, con Field, sus restricciones (gt, mayor que; le, menor o igual; min_length, longitud mínima). Los validadores añaden reglas propias: field_validator para un campo y model_validator para reglas entre campos, como que la media rápida sea más corta que la lenta.

IN [06]python
from pydantic import (BaseModel, ConfigDict, Field, ValidationError,
                      field_validator, model_validator)

class ConfigEstrategia(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    nombre: str
    simbolos: list[str] = Field(min_length=1)
    media_rapida: int = Field(gt=0)
    media_lenta: int = Field(gt=0)
    riesgo_por_operacion: float = Field(gt=0, le=0.02)
    coste_pb: float = Field(default=5.0, ge=0)
    lado: Literal["largo", "corto", "ambos"] = "largo"

    @field_validator("simbolos")
    @classmethod
    def mayusculas(cls, v: list[str]) -> list[str]:
        return [s.strip().upper() for s in v]

    @model_validator(mode="after")
    def rapida_menor(self) -> "ConfigEstrategia":
        if self.media_rapida >= self.media_lenta:
            raise ValueError("media_rapida debe ser < media_lenta")
        return self

cfg = ConfigEstrategia(nombre="cruce", simbolos=[" sint", "rvar"],
                       media_rapida="50", media_lenta=200,
                       riesgo_por_operacion=0.01)
print(cfg)
print(type(cfg.media_rapida).__name__)
OUT [06]Configuración de una estrategia con Pydantic
nombre='cruce' simbolos=['SINT', 'RVAR'] media_rapida=50 media_lenta=200 riesgo_por_operacion=0.01 coste_pb=5.0 lado='largo'
int

Pydantic ha hecho tres cosas: aplicar el validador de símbolos (sin espacios y en mayúsculas), convertir "50" en el entero 50 y rellenar los valores por defecto. La conversión de texto a número es el modo por defecto, llamado lax: acepta un valor si representa sin ambigüedad el tipo pedido. Con ConfigDict(strict=True) el texto se rechazaría.

model_config fija dos opciones importantes para una configuración. extra="forbid" convierte una clave desconocida en error, y frozen=True impide modificar el objeto una vez creado, igual que las dataclasses congeladas de la lección 0.4.1.

Errores de validación, todos a la vez

Cuando algo falla, Pydantic lanza ValidationError con todos los errores de los campos, no solo el primero. El mensaje completo es largo (incluye un enlace a la documentación por cada error); en un programa conviene recorrer errors(), que da una lista de diccionarios con la ubicación (loc) y el mensaje (msg) de cada fallo:

IN [07]python
def resumen_errores(datos: dict) -> None:
    try:
        ConfigEstrategia(**datos)
    except ValidationError as e:
        print(f"Errores en {e.title}: {e.error_count()}")
        for err in e.errors():
            campo = ".".join(str(p) for p in err["loc"]) or "(modelo)"
            print(f"  {campo}: {err['msg']}")

resumen_errores({"nombre": "cruce", "simbolos": ["SINT"],
                 "media_rapida": 50, "media_lenta": 200,
                 "riesgo_por_operacion": 0.2, "lado": "largos",
                 "coste_pbs": 3})
OUT [07]Tres errores en una configuración
Errores en ConfigEstrategia: 3
  riesgo_por_operacion: Input should be less than or equal to 0.02
  lado: Input should be 'largo', 'corto' or 'ambos'
  coste_pbs: Extra inputs are not permitted

Los tres errores son de los que se cuelan en la vida real: un riesgo diez veces mayor de lo previsto, un valor casi correcto y una clave mal escrita. Sin extra="forbid", coste_pbs se ignoraría en silencio y la estrategia operaría con el coste por defecto de 5 pb creyendo que usa 3.

La regla entre campos solo se evalúa si los campos han pasado su propia validación (mode="after" recibe el modelo ya construido). Con los campos correctos y las medias al revés:

IN [08]python
resumen_errores({"nombre": "cruce", "simbolos": ["SINT"],
                 "media_rapida": 200, "media_lenta": 50,
                 "riesgo_por_operacion": 0.01})
OUT [08]La regla entre campos
Errores en ConfigEstrategia: 1
  (modelo): Value error, media_rapida debe ser < media_lenta

Los mensajes de Pydantic están en inglés; el de la regla propia, en el idioma en que se escribió. La ubicación vacía indica que el error es del modelo entero, no de un campo.

Cargar la configuración desde YAML

YAML es el formato habitual de las configuraciones porque se lee y se edita a mano con facilidad (los formatos de fichero se ven en la lección 0.4.4). El flujo tiene dos pasos: yaml.safe_load convierte el texto en diccionarios y listas, y model_validate lo valida.

IN [09]python
import yaml

TEXTO = """
nombre: cruce-medias
simbolos: [sint, rvar, oro]
media_rapida: 50
media_lenta: 200
riesgo_por_operacion: 0.01
coste_pb: 2.5
"""

cfg = ConfigEstrategia.model_validate(yaml.safe_load(TEXTO))
print(cfg.simbolos, cfg.coste_pb, cfg.lado)
print(cfg.model_dump())
OUT [09]Configuración desde YAML
['SINT', 'RVAR', 'ORO'] 2.5 largo
{'nombre': 'cruce-medias', 'simbolos': ['SINT', 'RVAR', 'ORO'], 'media_rapida': 50, 'media_lenta': 200, 'riesgo_por_operacion': 0.01, 'coste_pb': 2.5, 'lado': 'largo'}

Usa siempre safe_load: crea solo tipos básicos. yaml.load con el cargador completo puede construir objetos arbitrarios de Python a partir del fichero, y una configuración no debe poder ejecutar código. model_dump() hace el camino inverso, de modelo a diccionario, útil para guardar junto a los resultados de un backtest la configuración exacta que los produjo.

Una trampa: model_copy(update=…) crea una copia con cambios sin validar. Sirve para valores que ya sabes que son correctos; para todo lo demás, hay que volver a validar:

IN [10]python
copia = cfg.model_copy(update={"riesgo_por_operacion": 0.5})
print(copia.riesgo_por_operacion)

nuevo = {**cfg.model_dump(), "riesgo_por_operacion": 0.5}
try:
    ConfigEstrategia.model_validate(nuevo)
except ValidationError as e:
    print(e.errors()[0]["msg"])
OUT [10]model_copy no valida
0.5
Input should be less than or equal to 0.02

La copia tiene un riesgo del 50 % por operación y ningún error lo ha impedido. En un sistema real, la configuración se valida una vez al arrancar, se congela y no se toca; si un parámetro cambia, se carga otra vez desde el fichero.

Ejercicios

  1. Añade a ConfigEstrategia un campo marco que solo admita "1d", "1h" y "5m", con "1d" por defecto. Comprueba que una configuración con marco: "15m" da un error en ese campo.
Solución
IN [11]python
class ConfigConMarco(ConfigEstrategia):
    marco: Literal["1d", "1h", "5m"] = "1d"

base = yaml.safe_load(TEXTO)
print(ConfigConMarco.model_validate(base).marco)
try:
    ConfigConMarco.model_validate({**base, "marco": "15m"})
except ValidationError as e:
    err = e.errors()[0]
    print(err["loc"], err["msg"])
OUT [11]Campo con valores permitidos
1d
('marco',) Input should be '1d', '1h' or '5m'

Un modelo de Pydantic se puede heredar como cualquier clase: la subclase conserva los campos, las opciones de model_config y los validadores de la base, y añade los suyos.

  1. Define un TypedDict Ejecucion con simbolo, cantidad (con signo) y precio, y una función anotada nocional(ejecuciones: list[Ejecucion]) -> float que sume el valor absoluto de cada ejecución. Pruébala con una compra de 100 a 142,22 y una venta de 40 a 145,10.
Solución
IN [12]python
class Ejecucion(TypedDict):
    simbolo: str
    cantidad: int
    precio: float

def nocional(ejecuciones: list[Ejecucion]) -> float:
    return sum(abs(e["cantidad"]) * e["precio"]
               for e in ejecuciones)

fills: list[Ejecucion] = [
    {"simbolo": "SINT", "cantidad": 100, "precio": 142.22},
    {"simbolo": "SINT", "cantidad": -40, "precio": 145.10},
]
print(f"{nocional(fills):,.2f}")
OUT [12]TypedDict de ejecuciones
20,026.00

14.222 + 5.804 = 20.026. El nocional negociado es la base sobre la que se calculan las comisiones proporcionales.

  1. Las configuraciones reales agrupan parámetros. Crea un modelo Costes (comision_pb y deslizamiento_pb, ambos mayores o iguales que cero) y un modelo Config con nombre y costes: Costes. Cárgalo desde un YAML con la sección costes anidada y calcula el coste total en puntos básicos.
Solución
IN [13]python
class Costes(BaseModel):
    model_config = ConfigDict(extra="forbid")
    comision_pb: float = Field(ge=0)
    deslizamiento_pb: float = Field(ge=0)

class Config(BaseModel):
    nombre: str
    costes: Costes

texto = """
nombre: cruce
costes:
  comision_pb: 1.5
  deslizamiento_pb: 3
"""
c = Config.model_validate(yaml.safe_load(texto))
print(c.costes)
print(c.costes.comision_pb + c.costes.deslizamiento_pb)
OUT [13]Modelos anidados
comision_pb=1.5 deslizamiento_pb=3.0
4.5

Pydantic valida el modelo anidado con sus propias reglas, y un error en él aparece con la ruta completa en loc, por ejemplo ('costes', 'comision_pb').

Footnotes

  1. PEP 649 y PEP 749, incluidas en Python 3.14: las anotaciones se guardan sin evaluar y se calculan al pedirlas. Detalles en la documentación de annotationlib. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. Una función está anotada como def importe(cantidad: int, precio: float) -> float. ¿Qué pasa al llamar a importe(2, "142.22")?

    • APython lanza TypeError porque "142.22" no es un float
    • BPython convierte el texto a float y devuelve 284.44
    • CPython ejecuta la función sin mirar las anotaciones y devuelve "142.22142.22"
    • DLa llamada no compila
    Ver respuesta

    Respuesta C. Python ejecuta la función sin mirar las anotaciones y devuelve "142.22142.22"

    Las anotaciones no se comprueban al ejecutar. 2 * "142.22" repite el texto. El error solo lo detecta un comprobador estático (mypy, ty) o una validación explícita como la de Pydantic.

  2. ¿Qué ventaja tiene Lado = Literal["compra", "venta"] frente a lado: str?

    • ANinguna: en tiempo de ejecución son iguales y el comprobador también los trata igual
    • BEl comprobador estático marca cualquier otro texto, como "comprar", antes de ejecutar
    • CPython lanza una excepción si se pasa otro texto
    • DHace el código más rápido
    Ver respuesta

    Respuesta B. El comprobador estático marca cualquier otro texto, como "comprar", antes de ejecutar

    Para Python, un Literal es un str normal; para mypy o ty, solo valen esos dos valores. En tiempo de ejecución hay que validarlo aparte, por ejemplo con get_args(Lado) o con Pydantic.

  3. Una configuración tiene media_rapida: int = Field(gt=0) y el YAML trae media_rapida: "50". ¿Qué hace Pydantic en su modo por defecto?

    • ALanza ValidationError porque el valor es texto
    • BAcepta el valor y lo guarda como texto
    • CConvierte "50" en el entero 50, porque el texto representa un entero válido
    • DIgnora el campo
    Ver respuesta

    Respuesta C. Convierte "50" en el entero 50, porque el texto representa un entero válido

    El modo por defecto (lax) convierte valores que representan sin ambigüedad el tipo pedido. Con strict=True el texto se rechazaría.

  4. Con una configuración Pydantic válida cfg, ¿qué hace cfg.model_copy(update={"riesgo_por_operacion": 0.5}) si el campo tiene le=0.02?

    • ALanza ValidationError
    • BDevuelve una copia con 0,5 sin validar: model_copy no pasa por los validadores
    • CDevuelve una copia con el valor recortado a 0,02
    • DModifica cfg en su sitio
    Ver respuesta

    Respuesta B. Devuelve una copia con 0,5 sin validar: model_copy no pasa por los validadores

    model_copy(update=…) copia y sustituye sin validar. Para cambiar un valor con garantías hay que volver a validar, por ejemplo con model_validate sobre el diccionario modificado.

Ideas clave

  • Las anotaciones documentan y permiten comprobar, pero Python no las aplica al ejecutar: 2 * "142.22" no falla.
  • Literal, TypedDict y Protocol describen valores permitidos, diccionarios con claves fijas e interfaces comunes sin herencia.
  • mypy o ty leen el código sin ejecutarlo y encuentran los errores de tipo antes de que lleguen a una cuenta real.
  • Pydantic valida en tiempo de ejecución, en la frontera: configuraciones, respuestas de APIs y ficheros. Con extra="forbid" una clave mal escrita es un error, no un valor por defecto silencioso.
  • Una configuración de estrategia se valida al cargarla, entera, y se congela. model_copy(update=…) no valida.

Recursos