Rendimiento de Pydantic: 4 consejos sobre cómo validar grandes cantidades de datos de manera eficiente

son tan fáciles de usar que también es fácil usarlos de manera incorrecta, como sostener un martillo por la cabeza. Lo mismo ocurre con Pydantic, una biblioteca de validación de datos de alto rendimiento para Python.

En Pydantic v2, el motor de validación central se implementa en Rust, lo que la convierte en una de las soluciones de validación de datos más rápidas del ecosistema Python. Sin embargo, esa ventaja de rendimiento solo se logra si usa Pydantic de una manera que realmente aproveche este núcleo altamente optimizado.

Este artículo se centra en el uso eficiente de Pydantic, especialmente al validar grandes volúmenes de datos. Destacamos cuatro errores comunes que pueden generar diferencias de rendimiento de orden de magnitud si no se controlan.

1) Prefiera las restricciones anotadas a los validadores de campo

Una característica central de Pydantic es que la validación de datos se define de forma declarativa en una clase de modelo. Cuando se crea una instancia de un modelo, Pydantic analiza y valida los datos de entrada de acuerdo con los tipos de campo y validadores definidos en esa clase.

El enfoque ingenuo: validadores de campo

Usamos un @field_validator para validar datos, como verificar si una columna de identificación es realmente un número entero o mayor que cero. Este estilo es legible y flexible, pero tiene un costo de rendimiento.

clase UserFieldValidators (BaseModel): id: int correo electrónico: etiquetas EmailStr: lista[str]

@field_validator(“id”) def _validate_id(cls, v: int) -> int: si no, isinstance(v, int): aumentar TypeError(“id debe ser un número entero”) si v < 1: aumentar ValueError("id debe ser >= 1″) return v @field_validator(“email”) def _validate_email(cls, v: str) -> str: si no isinstance(v, str): v = str(v) si no _email_re.match(v): elevar ValueError(“formato de correo electrónico no válido”) return v @field_validator(“tags”) def _validate_tags(cls, v: list[str]) -> lista[str]: si no es instancia(v, lista): genera TypeError(“las etiquetas deben ser una lista”) si no (1 <= len(v) <= 10): genera ValueError("la longitud de las etiquetas debe estar entre 1 y 10") para i, etiqueta en enumerate(v): si no es instancia(etiqueta, str): genera TypeError(f"etiqueta[{i}] debe ser una cadena") si etiqueta == "": elevar ValueError(f"etiqueta[{i}] no debe estar vacío")

La razón es que los validadores de campo se ejecutan en Python, después de la coerción del tipo central y la validación de restricciones. Esto evita que se optimicen o fusionen en el proceso de validación principal.

El enfoque optimizado: anotado

Podemos usar Anotado de la biblioteca de mecanografía de Python.

clase UserAnnotated(BaseModel): id: Anotado[int, Field(ge=1)]
correo electrónico: anotado[str, Field(pattern=RE_EMAIL_PATTERN)]
Etiquetas: Anotado[list[str]Campo(longitud_min=1, longitud_max=10)]

Esta versión es más corta, más clara y muestra una ejecución más rápida a escala.

Por qué Anotado es más rápido

Anotado (PEP 593) es una característica estándar de Python, de la biblioteca de mecanografía. Las restricciones colocadas dentro de Annotated se compilan en el esquema interno de Pydantic y se ejecutan dentro de pydantic-core (Rust).

Esto significa que no se requieren llamadas de validación de Python definidas por el usuario durante la validación. Además, no se introducen objetos Python intermedios ni flujos de control personalizados.

Por el contrario, las funciones @field_validator siempre se ejecutan en Python, introducen una sobrecarga de llamadas a funciones y, a menudo, duplican comprobaciones que podrían haberse manejado en la validación principal.

Matiz importante

Un matiz importante es que Annotated en sí no es “Rust”. La aceleración proviene del uso de restricciones que pydantic-core entiende y puede usar, no de que Annotated exista por sí solo.

Punto de referencia

La diferencia entre sin validación y validación anotada es insignificante en estos puntos de referencia, mientras que los validadores de Python pueden convertirse en una diferencia de orden de magnitud.

Gráfico de rendimiento de la validación (Imagen del autor)

Punto de referencia (tiempo en segundos) ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━ ━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┓ ┃ Método ┃ n=100 ┃ n=1k ┃ n=10k ┃ n=50k ┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━ ━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━┩ │Validadores de campo│ 0,004 │ 0,020 │ 0,194 │ 0,971 │ │ Sin validación │ 0,000 │ 0,001 │ 0,007 │ 0,032 │ │ Anotado │ 0,000 │ 0,001 │ 0,007 │ 0,036 │ └────────────────┴───────────┴── ────────┴───────────┴───────────┘

En términos absolutos pasamos de casi un segundo de tiempo de validación a 36 milisegundos. Un aumento de rendimiento de casi 30 veces.

Veredicto

Utilice Anotado siempre que sea posible. Obtendrá un mejor rendimiento y modelos más claros. Los validadores personalizados son poderosos, pero usted paga por esa flexibilidad en el costo del tiempo de ejecución, así que reserve @field_validator para la lógica que no se puede expresar como restricciones.

2). Validar JSON con model_validate_json()

Tenemos datos en forma de cadena JSON. ¿Cuál es la forma más eficiente de validar estos datos?

El enfoque ingenuo

Simplemente analiza el JSON y valida el diccionario:

py_dict = json.loads(j) UserAnnotated.model_validate(py_dict)

El enfoque optimizado

Utilice una función Pydantic:

UserAnnotated.model_validate_json(j)

¿Por qué esto es más rápido?

model_validate_json() analiza JSON y lo valida en una canalización Utiliza Pydantic interal y un analizador JSON más rápido Evita crear grandes diccionarios intermedios de Python y recorrer esos diccionarios por segunda vez durante la validación

Con json.loads() pagas dos veces: primero al analizar JSON en objetos Python, luego por validar y coaccionar esos objetos.

model_validate_json() reduce las asignaciones de memoria y el recorrido redundante.

Comparado

La versión Pydantic es casi el doble de rápida.

Gráfico de rendimiento (imagen del autor)

Punto de referencia (tiempo en segundos) ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━ ━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓ ┃ Método ┃ n=100 ┃ n=1K ┃ n=10K ┃ n=50K ┃ n=250K ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━ ━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩ │ Cargar json │ 0.000 │ 0.002 │ 0.016 │ 0.074 │ 0.368 │ │ validar modelo json │ 0.001 │ 0.001 │ 0.009 │ 0.042 │ 0,209│ └─────────────────────┴───────┴─ ──────┴───────┴───────┴────────┘

En términos absolutos el cambio nos ahorra 0,1 segundos al validar un cuarto de millón de objetos.

Veredicto

Si su entrada es JSON, deje que Pydantic se encargue del análisis y la validación en un solo paso. En cuanto al rendimiento, no es absolutamente necesario usar model_validate_json() pero hazlo de todos modos para evitar crear objetos Python intermedios y condensar tu código.

3) Utilice TypeAdapter para validación masiva

Tenemos un modelo de Usuario y ahora queremos validar una lista de Usuarios.

El enfoque ingenuo

Podemos recorrer la lista y validar cada entrada o crear un modelo contenedor. Supongamos que el lote es una lista[dict]:

# 1. Modelos de validación por artículo = [User.model_validate(item) for item in batch]

# 2. Modelo contenedor # 2.1 Definir un modelo contenedor: clase UserList(BaseModel): usuarios: lista[User]

# 2.2 Validar con el modelo contenedor models = UserList.model_validate({“usuarios”: lote}).usuarios

Enfoque optimizado

Los adaptadores de tipos son más rápidos para validar listas de objetos.

ta_annotated = TypeAdapter(lista[UserAnnotated]) modelos = ta_annotated.validate_python(lote)

¿Por qué esto es más rápido?

Deja el trabajo pesado a Rust. El uso de TypeAdapter no requiere la construcción de un Wrapper adicional y la validación se ejecuta utilizando un único esquema compilado. Hay menos cruces de límites de Python a Rust-and-back y hay una menor sobrecarga de asignación de objetos.

Los modelos contenedores son más lentos porque hacen más que validar la lista:

Construye una instancia de modelo adicional. Realiza un seguimiento de los conjuntos de campos y del estado interno. Maneja la configuración, los valores predeterminados y los extras.

Esa capa adicional es pequeña por llamada, pero se puede medir a escala.

Comparado

Cuando utilizamos conjuntos grandes, vemos que el adaptador de tipo es significativamente más rápido, especialmente en comparación con el modelo contenedor.

Gráfico de rendimiento (imagen del autor)

Punto de referencia (tiempo en segundos) ┏━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━ ━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ ┃ Método ┃ n=100 ┃ n=1K ┃ n=10K ┃ n=50K ┃ n=100K ┃ n=250K ┃ ┡━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━ ━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━┩ │ Por artículo │ 0,000 │ 0,001 │ 0,021 │ 0,091 │ 0,236 │ 0,502 │ │ Modelo de envoltura│ 0,000 │ 0,001 │ 0,008 │ 0,108 │ 0,208 │ 0,602 │ │ Adaptador de tipo │ 0,000 │ 0,001 │ 0,021 │ 0,083 │ 0,152 │ 0,381 │ └──────────────┴───────┴───────┴─ ──────┴───────┴────────┴────────┘

Sin embargo, en términos absolutos, la aceleración nos ahorra entre 120 y 220 milisegundos para 250.000 objetos.

Veredicto

Cuando solo desea validar un tipo, no definir un objeto de dominio, TypeAdapter es la opción más rápida y limpia. Aunque no es absolutamente necesario para ahorrar tiempo, evita la creación de instancias innecesarias del modelo y evita los bucles de validación del lado de Python, lo que hace que su código sea más limpio y legible.

4) Evite from_attributes a menos que lo necesite

Con from_attributes configuras tu clase de modelo. Cuando lo configura en Verdadero, le dice a Pydantic que lea valores de los atributos del objeto en lugar de claves del diccionario. Esto es importante cuando su entrada no es un diccionario, como una instancia ORM de SQLAlchemy, una clase de datos o cualquier objeto Python simple con atributos.

Por defecto, from_attributes es Falso. A veces los desarrolladores establecen este atributo en True para mantener el modelo flexible:

clase Producto(BaseModel): id: int nombre: str model_config = ConfigDict(from_attributes=True)

Sin embargo, si simplemente pasa diccionarios a su modelo, es mejor evitar from_attributes porque requiere que Python haga mucho más trabajo. La sobrecarga resultante no proporciona ningún beneficio cuando la entrada ya está en mapeo simple.

Por qué from_attributes=True es más lento

Este método utiliza getattr() en lugar de la búsqueda en el diccionario, que es más lento. También puede activar funcionalidades en el objeto que estamos leyendo, como descriptores, propiedades o carga diferida de ORM.

Punto de referencia

A medida que el tamaño de los lotes aumenta, el uso de atributos se vuelve cada vez más costoso.

Gráfico de rendimiento (imagen del autor)

Punto de referencia (tiempo en segundos) ┏━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━ ━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ ┃ Método ┃ n=100 ┃ n=1K ┃ n=10K ┃ n=50K ┃ n=100K ┃ n=250K ┃ ┡━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━ ━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━┩ │ con atributos │ 0,000 │ 0,001 │ 0,011 │ 0,110 │ 0,243 │ 0,593 │ │ sin atributos │ 0,000 │ 0,001 │ 0,012 │ 0,103 │ 0,196 │ 0,459 │ └──────────────┴───────┴───────┴─ ──────┴───────┴────────┴────────┘

En términos absolutos, se ahorra un poco menos de 0,1 segundos al validar 250.000 objetos.

Veredicto

Utilice from_attributes solo cuando su entrada no sea un dictado. Existe para admitir objetos basados ​​en atributos (ORM, clases de datos, objetos de dominio). En esos casos, puede ser más rápido que volcar primero el objeto en un dict y luego validarlo. Para asignaciones simples, agrega gastos generales sin ningún beneficio.

Conclusión

El objetivo de estas optimizaciones no es reducir unos milisegundos por sí solos. En términos absolutos, incluso una diferencia de 100 ms rara vez constituye un cuello de botella en un sistema real.

El valor real radica en escribir código más claro y utilizar correctamente las herramientas.

El uso de los consejos especificados en este artículo conduce a modelos más claros, una intención más explícita y una mejor alineación con la forma en que Pydantic está diseñado para funcionar. Estos patrones trasladan la lógica de validación del código Python ad-hoc a esquemas declarativos que son más fáciles de leer, razonar y mantener.

Las mejoras en el rendimiento son un efecto secundario de hacer las cosas de la manera correcta. Cuando las reglas de validación se expresan de forma declarativa, Pydantic puede aplicarlas de manera consistente, optimizarlas internamente y escalarlas de forma natural a medida que crecen sus datos.

En breve:

No adoptes estos patrones sólo porque son más rápidos. Adoptelos porque hacen que su código sea más simple, más explícito y más adecuado para las herramientas que está utilizando.

La aceleración es sólo una buena ventaja.

Espero que este artículo haya sido tan claro como pretendía, pero si este no es el caso, hágame saber qué puedo hacer para aclararlo más. Mientras tanto, consulte mis otros artículos sobre todo tipo de temas relacionados con la programación.

¡Feliz codificación!

—Mike

Pd: ¿te gusta lo que estoy haciendo? ¡Sígueme!