Introducción A Pydantic

Fecha de publicación: 13 Marzo 2021
Tiempo de lectura: 6 min.
Premium: False
Número de visitas: 246


Las validaciones regularmente suele ser un dolor de cabeza para muchos desarrolladores (Me incluyo en ellos). Hay ocasiones en las cuales podemos llegar a pensar que nuestro desarrollo ha finalizado, pero o sorpresa, resulta que nos hizo falta contemplar un par de escenarios y hemos olvidado implementar ciertas validaciones.

En el mejor de los casos basta con añadir un par de condiciones y listo, pero en el peor de ellos hay que re escribir todo lo que hemos hecho. Algo que sin duda no le deseo a nadie. ☹

Afortunadamente existen un par de librerías en Python que nos permiten implementar validaciones de una forma muy sencilla. Y para este post me gustaría habláramos acerca de una de ellas. Me refiero a pydantic, una librería que nos permite la validación de datos y gestión de configuraciones mediante anotaciones.

Es una librería muy muy interesante, que de hecho Frameworks web como lo es FastAPI se apoyan de ella para el proceso de desarrollo. 🤓 Así que vale la pena echarle un vistazo.

Si aun no tienes muy en claro que son las anotaciones, te comparto un link donde te lo explicamos más en detalle. 😺

Bien, sin más introducción, comencemos con el tutorial.

Pydantic

Comenzamos con el proceso de instalación. Verás, pydantic no es una librería de la biblioteca estándar de Python, así que será necesario instalarla. Para ello ejecutamos el siguiente comando.

pip install pydantic

Listo, ahora pasemos al tema de validaciones, por lo cual pydantic sale a relucir.

Para nosotros poder validar tanto nuestros datos de entrada como de salida nos apoyaremos de la clase BaseModel, de la cual debemos de heredar.

Veamos un ejemplo.

from pydantic import BaseModel

from typing import Optional

class User(BaseModel): 
    username: str
    password: str
    email: str
    age: Optional[int] = None

En este caso nuestra clase User hereda de la clase BaseModel, lo cual nos garantiza que los valores que serán almacenados para cada uno de los atributos corresponderán al tipo de dato que hayamos definido en las anotaciones.

En mi caso he indicado que los atributos: username, password y email solo podrán almacenar valores de tipo strings y serán obligatorios, por otra parte el atributo age será opcional y solo podrá almacenar valores de tipo entero.

Con esta simple definición de clase ya hemos validado los tipos de datos a utilizar. 😎

Ahora procedamos a crear una instancia.

>>> user = User(username='Eduardo', password='Password123', email='eduardo78d@gmail.com')
>>> user
username='Eduardo' password='Password123' email='eduardo78d@gmail.com' age=None

También podemos crear objetos a través de diccionarios. Algo que muchos a veces olvidamos. 😉

>>> user = {
    'username':' Eduardo',
    'password': 'Password123',
    'email': 'eduardo78d@gmail.com'
}

>>> user = User(**user)
>>> user
username='Eduardo' password='Password123' email='eduardo78d@gmail.com' age=None

Y ahora,¿Qué pasa si intentamos crear un objeto cuyos atributos no cumplan con los tipos de datos definidos? Pues bien, en esos casos obtendremos un error.

>>> user = User(username='Eduardo', password='password')

pydantic.error_wrappers.ValidationError: 1 validation error for User
email
  field required (type=value_error.missing)

En este caso se lanza un error para el atributo email, donde se indica que el capo es requerido.

Para obtener una salida mucho más detallada de los posibles errores, es recomendable intentar crear la instancia mediante un try y un catch.

from pydantic import ValidationError

try:
    user = User(username='Eduardo', password='password')

except ValidationError as e:
    print(e.json())

Esto dará como salida:

[
  {
    "loc": [
      "email"
    ],
    "msg": "field required",
    "type": "value_error.missing"
  }
]

Un listado de errores en formato JSON. Algo que desde mi punto de vista se lee mucho mejor. 😃


Algo curioso que me gustaría mencionar ocurre al intentar utilizar enteros donde deberían ir strings. Por default pydantic hará la conversión, de enteros a strings, que si bien en primera instancia puede sonar algo super cool, habrá ocasiones en las cuales no se desee este comportamiento, así que hay que tener mucho cuidado allí.

# De Strings a enteros

>>> user = User(username='Eduardo', password=123, email=122333)
>>> user
username='Eduardo' password='123' email='122333' age=None

Validaciones propias

Ok, esta aquí todo bien, sin embargo ¿Qué pasa si debemos implementar ciertas reglas de negocios sobre los atributos de nuestra clase y no solo validar sus tipos de datos? Bueno, en estos casos nos podemos apoyar del decorador validator, el cual nos permitirá implementar nuestras propias validaciones.

Por ejemplo, validemos que el atributo username deba poseer una longitud mínima de 4 caracteres y una longitud máxima de 50. En caso el string no cumpla con estas reglas no será posible crear el objeto.

Para implementar esta validación nuestras clase pudiera quedar de la siguiente manera.

from pydantic import validator
from pydantic import BaseModel

class User(BaseModel): 
    username: str
    password: str
    email: str
    age: Optional[int] = None


    @validator('username')
    def username_length(cls, username):

        if len(username) < 4:
            raise ValueError('La longitud mínima  es de 4 caracteres.')

        if len(username) > 50:
            raise ValueError('La longitud máxima es de 50 caracteres.')

        return username

El decorador recibe como argumento el nombre del atributo el cual deseamos validar, además que, el decorador deberá decorar no un método de instancia, si no un método de clase. Por su parte, el método de clase poseerá como parámetro el valor del atributo que vamos a condicionar.

Si intentamos crear el objeto:

try:
    user = User(username='123', password='', email='eduardo78d@gmail.com')
except ValidationError as e:
    print(e.json())

Obtendremos la siguiente salida:

[
  {
    "loc": [
      "username"
    ],
    "msg": "La longitud m\u00ednima  es de 4 caracteres.",
    "type": "value_error"
  }
]

Errores de encoding 😰, pero fuera de eso nuestra validación funciona baste bien.

En caso alguna de nuestras validaciones depende de otros atributos sepamos que podemos acceder a ellos. Para esto será necesario definir un par más de parámetros para nuestro método de clase.

Por ejemplo, imaginemos que nos encontramos en el proceso para la creación de un nuevo usuario, y para ello debemos validar que las contraseñas que ha ingresado el usuario sean las mismas. Para ello nuestro modelo pudiera quedar de la siguiente manera.

from pydantic import validator
from pydantic import BaseModel

class User(BaseModel):
    username : str
    password : str
    confirm_password : str
    email: str

    @validator('confirm_password')
    def passwords_match(cls, confirm_password, values, **kwargs):

        if 'password' in values and confirm_password != values['password']:
            raise ValueError('Las contraseñas no coinciden')

        return confirm_password

En este caso como podemos observar, hemos definimos un nuevo parámetro para nuestro método de clase, values. Este parámetro no es más que un diccionario; en donde podremos encontrar los atributos del objeto con sus correspondientes valores. Mediante este diccionario podremos validar sobre múltiples atributos. 🕵️‍♂️

try:
    user = User(username='eduardo', 
                password='password123', 
                confirm_password='password',
                email='eduardo78d@gmail.com')

except ValidationError as e:
    print(e.json())

Salida:

[
  {
    "loc": [
      "confirm_password"
    ],
    "msg": "Las contrase\u00f1as no coinciden",
    "type": "value_error"
  }
]

Validar Email

Algo muy común en el desarrollo es validar correos electrónicos, que si bien podemos hacerlo utilizando una validación propia, déjame decirte que pydantic ya posee su propio módulo para estos casos.

Para ello vamos a instalar la extensión Email.

pip install pydantic[email]

Y modificamos el tipo de datos de nuestro atributo email.

from pydantic import BaseModel, EmailStr


class User(BaseModel): 
    username: str
    password: str
    email: EmailStr
    ....

Dejamos a un lado str y ahora utilizamos la clase EmailStr. A través de esta clase podemos validar que el nuestro atributo email cumpla con los requerimientos necesarios para considerarse un correo electrónico valido. 🤓

Tipos de datos a utilizar

Afortunadamente pydantic nos provee de un soporte muy amplio para los tipos de datos más comunes en Python, de los cuales destacan.

  • bool
  • int
  • str
  • float
  • bytes
  • list
  • tuple
  • dict
  • set
  • datetime.date
  • datetime.time
  • datetime.datetime
  • typing.Any
  • typing.Optional
  • typing.List
  • typing.Tuple
  • typing.Dict
  • typing.Set

Métodos y atributos

Ya para finalizar me gustaría mencionar que, la clase BaseModel posee un par de métodos y atributos que sin duda nos pueden ser de mucha utilidad en nuestro proceso de desarrollo. Veamos, aquí los más interesantes desde mi punto de vista.

Métodos:

  • dict(): Retorna un diccionario con cada uno de los atributos de nuestra clase con sus correspondientes valores.
  • json(): Retorna un string en representación de un objeto JSON para nuestro diccionario de atributos.
  • construct(): Método de clase que nos permite instanciar objetos sin ejecutar validaciones.

Atributos:

  • \fields\: Diccionario de todos los atributos de la clase.
  • _\fields_set\: Set de nombres de los atributos de la clase.

Conclusión

En conclusión pydantic es una excelente librería para nosotros poder validar tanto la entrada como salida de datos. Y no solo eso, si no además, como pudimos observar, la librería nos permite serializar nuestros objetos para que el envío de datos sea mucho más sencillo.

Recomiendo ampliamente pydantic para cuando nos encontremos trabajando ya sea con un ORM o al desarrollar alguna API, ya sea con FastAPI, o por que no, con Flask Mismo. 🤓

Más Tips y Ejercicios 🐍

Adquiere una subscripción PyWombat por tan solo $3 USD. al mes.

Conoce los beneficios de ser usuario premium:
Niveles desbloqueados: Ten accesos a todos los niveles de ejercicios. 🔓
Nuevo límite: Incrementa tu límite de ejercicios por semana. 🚀
Contenido único: Recibe semanalmente recursos exclusivos de Python (Videos, Artículos y Capitulos del libro PyWombat, comienza como desarrollador Python. 🐍