1kbln4d0mcua9locop25yya.jpeg

¿Qué pasa con los valores predeterminados y las extracciones de argumentos?

from pydantic import validate_call

@validate_call(validate_return=True)
def add(*args: int, a: int, b: int = 4) -> int:
return str(sum(args) + a + b)

# ----
add(4,3,4)
> ValidationError: 1 validation error for add
a
Missing required keyword only argument [type=missing_keyword_only_argument, input_value=ArgsKwargs((4, 3, 4)), input_type=ArgsKwargs]
For further information visit <https://errors.pydantic.dev/2.5/v/missing_keyword_only_argument>

# ----

add(4, 3, 4, a=3)
> 18

# ----

@validate_call
def add(*args: int, a: int, b: int = 4) -> int:
return str(sum(args) + a + b)

# ----

add(4, 3, 4, a=3)
> '18'

Conclusiones de este ejemplo:

  • Puede anotar el tipo de la declaración del número variable de argumentos (*args).
  • Los valores predeterminados siguen siendo una opción, incluso si está anotando tipos de datos variables.
  • validate_call acepta validate_return argumento, que también permite validar el valor de retorno de la función. En este caso también se aplica la coerción de tipo de datos. validate_return se establece en False de forma predeterminada. Si se deja como está, es posible que la función no devuelva lo que se declara en la sugerencia de tipo.

¿Qué sucede si desea validar el tipo de datos pero también restringir los valores que puede tomar esa variable? Ejemplo:

from pydantic import validate_call, Field
from typing import Annotated

type_age = Annotated[int, Field(lt=120)]

@validate_call(validate_return=True)
def add(age_one: int, age_two: type_age) -> int:
return age_one + age_two

add(3, 300)
> ValidationError: 1 validation error for add
1
Input should be less than 120 [type=less_than, input_value=200, input_type=int]
For further information visit <https://errors.pydantic.dev/2.5/v/less_than>

Este ejemplo muestra:

  • Puedes usar Annotated y pydantic.Field no sólo validar el tipo de datos, sino también agregar metadatos que Pydantic utiliza para restringir los valores y formatos de las variables.
  • ValidationError Una vez más, explica con gran detalle qué fue lo que falló en nuestra llamada de función. Esto puede resultar muy útil.

A continuación, se muestra un ejemplo más de cómo se pueden validar y restringir los valores de las variables. Simularemos una carga útil (diccionario) que desea procesar en su función después de que se haya validado:

from pydantic import HttpUrl, PastDate
from pydantic import Field
from pydantic import validate_call
from typing import Annotated

Name = Annotated[str, Field(min_length=2, max_length=15)]

@validate_call(validate_return=True)
def process_payload(url: HttpUrl, name: Name, birth_date: PastDate) -> str:
return f'{name=}, {birth_date=}'

# ----

payload = {
'url': 'httpss://example.com',
'name': 'J',
'birth_date': '2024-12-12'
}

process_payload(**payload)
> ValidationError: 3 validation errors for process_payload
url
URL scheme should be 'http' or 'https' [type=url_scheme, input_value='httpss://example.com', input_type=str]
For further information visit <https://errors.pydantic.dev/2.5/v/url_scheme>
name
String should have at least 2 characters [type=string_too_short, input_value='J', input_type=str]
For further information visit <https://errors.pydantic.dev/2.5/v/string_too_short>
birth_date
Date should be in the past [type=date_past, input_value='2024-12-12', input_type=str]
For further information visit <https://errors.pydantic.dev/2.5/v/date_past>

# ----

payload = {
'url': '<https://example.com>',
'name': 'Joe-1234567891011121314',
'birth_date': '2020-12-12'
}

process_payload(**payload)
> ValidationError: 1 validation error for process_payload
name
String should have at most 15 characters [type=string_too_long, input_value='Joe-1234567891011121314', input_type=str]
For further information visit <https://errors.pydantic.dev/2.5/v/string_too_long>

Estos fueron los conceptos básicos de cómo validar los argumentos de una función y su valor de retorno.

Ahora, pasaremos a la segunda forma más importante en que se puede utilizar Pydantic para validar y procesar datos: mediante la definición de modelos.

Esta parte es más interesante a efectos de tratamiento de datos, como verás.

Hasta ahora hemos utilizado validate_call para decorar funciones y argumentos de funciones especificados y sus tipos y restricciones correspondientes.

Aquí, definimos modelos definiendo clases de modelos, donde especificamos campos, sus tipos y restricciones. Esto es muy similar a lo que hicimos anteriormente. Al definir una clase de modelo que hereda de Pydantic BaseModelUtilizamos un mecanismo oculto que realiza la validación, el análisis y la serialización de los datos. Esto nos brinda la capacidad de crear objetos que se ajusten a las especificaciones del modelo.

Aquí hay un ejemplo:

from pydantic import Field
from pydantic import BaseModel

class Person(BaseModel):
name: str = Field(min_length=2, max_length=15)
age: int = Field(gt=0, lt=120)

# ----

john = Person(name='john', age=20)
> Person(name='john', age=20)

# ----

mike = Person(name='m', age=0)
> ValidationError: 2 validation errors for Person
name
String should have at least 2 characters [type=string_too_short, input_value='j', input_type=str]
For further information visit <https://errors.pydantic.dev/2.5/v/string_too_short>
age
Input should be greater than 0 [type=greater_than, input_value=0, input_type=int]
For further information visit <https://errors.pydantic.dev/2.5/v/greater_than>

También puedes utilizar anotaciones aquí y especificar valores predeterminados para los campos. Veamos otro ejemplo:

from pydantic import Field
from pydantic import BaseModel
from typing import Annotated

Name = Annotated[str, Field(min_length=2, max_length=15)]
Age = Annotated[int, Field(default=1, ge=0, le=120)]

class Person(BaseModel):
name: Name
age: Age

# ----

mike = Person(name='mike')
> Person(name='mike', age=1)

Las cosas se ponen muy interesantes cuando el caso de uso se vuelve un poco complejo. Recuerde que payload ¿Qué hemos definido? Voy a definir otra estructura más compleja que vamos a revisar y validar. Para hacerlo más interesante, vamos a crear una carga útil que usaremos para consultar un servicio que actúa como intermediario entre nosotros y los proveedores de LLM. Luego la validaremos.

Aquí hay un ejemplo:

from pydantic import Field
from pydantic import BaseModel
from pydantic import ConfigDict

from typing import Literal
from typing import Annotated
from enum import Enum

payload = {
"req_id": "test",
"text": "This is a sample text.",
"instruction": "embed",
"llm_provider": "openai",
"llm_params": {
"llm_temperature": 0,
"llm_model_name": "gpt4o"
},
"misc": "what"
}

ReqID = Annotated[str, Field(min_length=2, max_length=15)]

class LLMProviders(str, Enum):
OPENAI = 'openai'
CLAUDE = 'claude'

class LLMParams(BaseModel):
temperature: int = Field(validation_alias='llm_temperature', ge=0, le=1)
llm_name: str = Field(validation_alias='llm_model_name',
serialization_alias='model')

class Payload(BaseModel):
req_id: str = Field(exclude=True)
text: str = Field(min_length=5)
instruction: Literal['embed', 'chat']
llm_provider: LLMProviders
llm_params: LLMParams

# model_config = ConfigDict(use_enum_values=True)

# ----

validated_payload = Payload(**payload)
validated_payload
> Payload(req_id='test',
text='This is a sample text.',
instruction='embed',
llm_provider=<LLMProviders.OPENAI: 'openai'>,
llm_params=LLMParams(temperature=0, llm_name='gpt4o'))

# ----

validated_payload.model_dump()
> {'text': 'This is a sample text.',
'instruction': 'embed',
'llm_provider': <LLMProviders.OPENAI: 'openai'>,
'llm_params': {'temperature': 0, 'llm_name': 'gpt4o'}}

# ----

validated_payload.model_dump(by_alias=True)
> {'text': 'This is a sample text.',
'instruction': 'embed',
'llm_provider': <LLMProviders.OPENAI: 'openai'>,
'llm_params': {'temperature': 0, 'model': 'gpt4o'}}

# ----

# After adding
# model_config = ConfigDict(use_enum_values=True)
# in Payload model definition, you get

validated_payload.model_dump(by_alias=True)
> {'text': 'This is a sample text.',
'instruction': 'embed',
'llm_provider': 'openai',
'llm_params': {'temperature': 0, 'model': 'gpt4o'}}

Algunas de las ideas importantes que se desprenden de este ejemplo elaborado son:

  • Puedes utilizar enumeraciones o Literal para definir una lista de valores específicos que se esperan.
  • En caso de que desee nombrar el campo de un modelo de manera diferente al nombre del campo en los datos validados, puede usar validation_alias. Especifica el nombre del campo en los datos que se están validando.
  • serialization_alias Se utiliza cuando el nombre del campo interno del modelo no es necesariamente el mismo nombre que desea utilizar al serializar el modelo.
  • El campo se puede excluir de la serialización con exclude=True.
  • Los campos de modelo también pueden ser modelos de Pydantic. El proceso de validación en ese caso se realiza de forma recursiva. Esta parte es realmente genial, ya que Pydantic se encarga de profundizar mientras valida las estructuras anidadas.
  • Los campos que no se tienen en cuenta en la definición del modelo no se analizan.

Aquí te mostraré fragmentos de código que muestran dónde y cómo puedes usar Pydantic en tus tareas diarias.

Supongamos que tiene datos que necesita validar y procesar. Se pueden almacenar en archivos CSV, Parquet o, por ejemplo, en una base de datos NoSQL en forma de documento. Tomemos el ejemplo de un archivo CSV y supongamos que desea procesar su contenido.

Aquí está el archivo CSV (test.csv) ejemplo:

name,age,bank_account
johnny,0,20
matt,10,0
abraham,100,100000
mary,15,15
linda,130,100000

Y así es como se valida y analiza:

from pydantic import BaseModel
from pydantic import Field
from pydantic import field_validator
from pydantic import ValidationInfo
from typing import List
import csv

FILE_NAME = 'test.csv'

class DataModel(BaseModel):
name: str = Field(min_length=2, max_length=15)
age: int = Field(ge=1, le=120)
bank_account: float = Field(ge=0, default=0)

@field_validator('name')
@classmethod
def validate_name(cls, v: str, info: ValidationInfo) -> str:
return str(v).capitalize()

class ValidatedModels(BaseModel):
validated: List[DataModel]

validated_rows = []

with open(FILE_NAME, 'r') as f:
reader = csv.DictReader(f, delimiter=',')
for row in reader:
try:
validated_rows.append(DataModel(**row))
except ValidationError as ve:
# print out error
# disregard the record
print(f'{ve=}')

validated_rows
> [DataModel(name='Matt', age=10, bank_account=0.0),
DataModel(name='Abraham', age=100, bank_account=100000.0),
DataModel(name='Mary', age=15, bank_account=15.0)]

validated = ValidatedModels(validated=validated_rows)
validated.model_dump()
> {'validated': [{'name': 'Matt', 'age': 10, 'bank_account': 0.0},
{'name': 'Abraham', 'age': 100, 'bank_account': 100000.0},
{'name': 'Mary', 'age': 15, 'bank_account': 15.0}]}

FastAPI ya está integrado con Pydantic, por lo que este artículo será muy breve. La forma en que FastAPI maneja las solicitudes es pasándolas a una función que maneja la ruta. Al pasar esta solicitud a una función, la validación se realiza automáticamente. Algo similar a la función validation_call que mencionamos al principio de este artículo.

Ejemplo de app.py que se utiliza para ejecutar el servicio basado en FastAPI:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

class Request(BaseModel):
request_id: str
url: HttpUrl

app = FastAPI()

@app.post("/search/by_url/")
async def create_item(req: Request):
return item

Pydantic es una biblioteca muy potente y tiene muchos mecanismos para una multitud de casos de uso diferentes y también casos extremos. Hoy expliqué las partes más básicas de cómo debería usarse y, a continuación, proporcionaré referencias para aquellos que no sean pusilánimes.

Sal y explora. Estoy seguro de que te resultará útil en diferentes aspectos.