Python SQLAlchemy Pydantic en la práctica: el código y los comandos que realmente importan

Python SQLAlchemy Pydantic : lo esencial en un artículo — código real, esquemas y pasos concretos, extractos de un curso de 27 lecciones.

Python SQLAlchemy Pydantic en la práctica: el código y los comandos que realmente importan

Sin teoría interminable aquí: abrimos la terminal y practicamos. Aquí lo esencial de Python SQLAlchemy Pydantic, extraído directamente de un curso completo de 27 lecciones — con código real que puedes copiar y pegar ahora.

tl;dr
  • Introducción
  • SQLAlchemy Core
  • ORM Declarative
  • Sesión y transacciones
  • Consultas avanzadas
~$ cat ./parcours.md # Python SQLAlchemy Pydantic — 9 capítulos
01
Introducción
→ SQLAlchemy 2.0, la nueva era→ Pydantic v2 y el tipado estricto
02
SQLAlchemy Core
→ Engine y conexión→ Tablas y Metadata+ 1 más lecciones
03
ORM Declarativo
→ Mapped y mapped_column→ Tipos y restricciones+ 2 más lecciones
04
Sesión y transacciones
→ Patrón Session→ Transacciones y savepoints+ 1 más lecciones
05
Consultas avanzadas
→ Joins, CTE, subconsultas→ Agregaciones y analytics+ 1 más lecciones
06
Pydantic v2
→ BaseModel y Field→ Validators y campos calculados+ 1 más lecciones
07
SQLAlchemy + Pydantic
→ Mapper SQLAlchemy <-> Pydantic→ Pydantic Settings
08
Migraciones y pruebas
→ Migraciones Alembic→ Tests con pytest y factories+ 1 más lecciones
🏁
Proyecto final (+ 1 capítulos en camino)
→ Te vas con un proyecto concreto y demostrable

Factories con factory-boy

NOTEObjetivo — Reemplazar los datos de prueba escritos a mano por factories declarativas. Vas a generar objetos SQLAlchemy realistas, coherentes y reproducibles con factory-boy y Faker, sin duplicar código en cada test.

Objetivos pedagógicos

TIPAl finalizar este módulo
  • Explicar por qué las fixtures escritas a mano se vuelven inmanejables
  • Definir una SQLAlchemyModelFactory vinculada a una sesión
  • Usar Faker, Sequence y SubFactory para datos realistas
  • Gestionar las relaciones (one-to-many) mediante RelatedFactoryList
  • Conectar las factories a las fixtures de pytest del módulo anterior

La intuición: una fábrica en lugar de un formulario vacío

En un test necesitas un objeto User válido. Escrito a mano, esto da User(name="Test", email="a@b.c", age=30, is_active=True, ...). Repetido en 40 tests, es 40 veces la misma tarea, y el día que añadas una columna NOT NULL, los 40 tests fallan.

Una factory es una fábrica: describes una sola vez cómo fabricar un User típico, luego cada test pide UserFactory() y recibe un objeto completo y válido. Si un test necesita un caso particular, solo sobrecarga el campo en cuestión: UserFactory(age=17). Todo lo demás se rellena automáticamente.

Declarativo

Describes el modelo tipo, no cada instancia. El código de test sigue siendo corto y legible.

Realista

Faker genera nombres, emails y direcciones plausibles en lugar de "aaa".

Reproducible

Se puede fijar la semilla (seed) para obtener los mismos datos en cada ejecución.

Instalación y primera factory

Instalamos factory-boy (que incluye Faker) :

WARNINGAtención al N+1 de creaciónRelatedFactoryList con size=100 inserta 100 objetos uno por uno. Para grandes volúmenes de seed, prefiere create_batch o una inserción masiva.

Conectar las factories a pytest

Reutilizamos la fixture db_session del módulo anterior (rollback automático). Inyectamos esta sesión en las factories y luego exponemos fixtures prácticas.

Consultas complejas y reporting

NOTEObjetivo — Construir la capa analítica del ERP: cifra de negocio por cliente, clasificación de productos, evolución mensual. Combina las consultas avanzadas del Capítulo 04 con los modelos del proyecto y expone los resultados mediante esquemas Pydantic dedicados al reporting.

Objetivos pedagógicos

TIPAl finalizar este módulo
  • Escribir una agregación multi-tabla (clientes, pedidos, líneas) con GROUP BY
  • Clasificar productos con una función de ventana rank()
  • Estructurar una consulta legible con una CTE
  • Mapear un resultado heterogéneo (Row) a un esquema Pydantic de reporting
  • Validar estas consultas mediante tests sobre datos de factory

La intuición: separar lo transaccional de lo analítico

Hasta ahora, el ERP crea pedidos (transaccional). El reporting responde a otras preguntas: ¿quiénes son mis mejores clientes? ¿qué producto se vende mejor este mes? Estas consultas no modifican nada; agregan y clasifican. Se aíslan en un módulo reporting/ para no mezclar responsabilidades.

Servicios transaccionales

Crean y modifican entidades (realizar pedido, decrementar stock). Una sesión, una transacción.

Consultas de reporting

Solo lectura, agregaciones pesadas. Devuelven filas (Row) en lugar de entidades ORM.

Cifra de negocio por cliente

Une Customer, Order y OrderLine, luego suma quantity * unit_price. Usa la API select() 2.0 y func para la agregación.

Proyecto ERP - Modelos

Proyecto final • Pliego de condiciones • modelos ORM

NOTEObjetivo — Diseñar un mini-ERP: clientes, productos, pedidos, facturas, stock.

Especificaciones

NOTEFuncionalidades :
  • Gestionar clientes (B2B/B2C) con direcciones
  • Catálogo de productos con stock multi-almacén
  • Pedidos multilínea con estado workflow
  • Facturación automática con IVA
  • Movimientos de stock auditados
  • Informes por mes / por cliente / por producto

Estructura del proyecto

output
erp/
├── alembic.ini
├── migrations/
├── app/
│   ├── core/
│   │   ├── config.py            # Configuración
│   │   └── database.py
│   ├── models/
│   │   ├── customer.py
│   │   ├── product.py
│   │   ├── order.py
│   │   └── invoice.py
│   ├── schemas/                 # Pydantic In/Out
│   ├── services/                # Lógica de negocio
│   ├── api/                     # Rutas FastAPI
│   └── main.py
└── tests/

Modelo Customer

output
import enum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from datetime import datetime

class CustomerType(str, enum.Enum):
    B2B = "b2b"
    B2C = "b2c"

class Customer(Base):
    __tablename__ = "customers"
    id: Mapped[int] = mapped_column(primary_key=True)
    type: Mapped[CustomerType]
    name: Mapped[str]
    email: Mapped[str] = mapped_column(unique=True)
    tax_id: Mapped[str | None]              # para B2B
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
    
    addresses: Mapped[list["Address"]] = relationship(back_populates="customer", cascade="all")
    orders: Mapped[list["Order"]] = relationship(back_populates="customer")

class Address(Base):
    __tablename__ = "addresses"
    id: Mapped[int] = mapped_column(primary_key=True)
    customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
    street: Mapped[str]
    city: Mapped[str]
    zip_code: Mapped[str]
    country: Mapped[str]
    is_billing: Mapped[bool] = mapped_column(default=False)
    is_shipping: Mapped[bool] = mapped_column(default=True)
    
    customer: Mapped["Customer"] = relationship(back_populates="addresses")

Modelo Product y Stock

output
from decimal import Decimal

class Product(Base):
    __tablename__ = "products"
    id: Mapped[int] = mapped_column(primary_key=True)
    sku: Mapped[str] = mapped_column(unique=True)
    name: Mapped[str]
    price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    vat_rate: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("20.00"))
    active: Mapped[bool] = mapped_column(default=True)
    
    stocks: Mapped[list["Stock"]] = relationship(back_populates="product")

class Warehouse(Base):
    __tablename__ = "warehouses"
    id: Mapped[int] = mapped_column(primary_key=True)
    code: Mapped[str] = mapped_column(unique=True)
    name: Mapped[str]

class Stock(Base):
    __tablename__ = "stocks"
    id: Mapped[int] = mapped_column(primary_key=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
    warehouse_id: Mapped[int] = mapped_column(ForeignKey("warehouses.id"))
    quantity: Mapped[int] = mapped_column(default=0)
    
    __table_args__ = (
        UniqueConstraint("product_id", "warehouse_id"),
        CheckConstraint("quantity >= 0"),
    )
    
    product: Mapped["Product"] = relationship(back_populates="stocks")

Modelo Order y OrderLine

output
class OrderStatus(str, enum.Enum):
    DRAFT = "draft"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class Order(Base):
    __tablename__ = "orders"
    id: Mapped[int] = mapped_column(primary_key=True)
    customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
    reference: Mapped[str] = mapped_column(unique=True)
    status: Mapped[OrderStatus] = mapped_column(default=OrderStatus.DRAFT)
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
    
    customer: Mapped["Customer"] = relationship(back_populates="orders")
    lines: Mapped[list["OrderLine"]] = relationship(back_populates="order", cascade="all, delete-orphan")
    invoice: Mapped["Invoice"] = relationship(back_populates="order", uselist=False)

class OrderLine(Base):
    __tablename__ = "order_lines"
    id: Mapped[int] = mapped_column(primary_key=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
    product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
    quantity: Mapped[int] = mapped_column()
    unit_price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    
    order: Mapped["Order"] = relationship(back_populates="lines")
    product: Mapped["Product"] = relationship()

Modelo Invoice y StockMovement

output
class Invoice(Base):
    __tablename__ = "invoices"
    id: Mapped[int] = mapped_column(primary_key=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), unique=True)
    number: Mapped[str] = mapped_column(unique=True)
    issued_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
    subtotal: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    vat: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    total: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    paid: Mapped[bool] = mapped_column(default=False)
    
    order: Mapped["Order"] = relationship(back_populates="invoice")

class StockMovement(Base):
    __tablename__ = "stock_movements"
    id: Mapped[int] = mapped_column(primary_key=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
    warehouse_id: Mapped[int] = mapped_column(ForeignKey("warehouses.id"))
    quantity: Mapped[int]                            # negativo = salida
    reason: Mapped[str]                              # "order_123", "restock"
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)

Resumen

NOTEPara recordar
  • Modelos separados por dominio (customer, order, product)
  • Decimal para el dinero (nunca float)
  • Enum para los estados workflow
  • cascade para las agregaciones (Order -> lines)
  • StockMovement = audit log inmutable
va-plus-loin

Este artículo cubre los extractos más útiles — el curso completo Python SQLAlchemy Pydantic (9 capítulos, 27 lecciones, ejercicios corregidos y proyecto final) te lleva hasta el final.

./acceder-au-cours-complet cours gratuit : Vibe Coding

FAQ

¿Cuánto tiempo se necesita para aprender Python SQLAlchemy Pydantic?
Con una progresión estructurada (9 capítulos, 27 lecciones cortas y prácticas), se alcanza un nivel operativo en unas semanas a razón de 30 a 60 minutos al día. Lo importante es practicar cada concepto de inmediato.
¿Se necesitan requisitos previos?
Básicos de informática son suficientes. Si sabes usar una terminal y leer código simple, estás listo.
¿Por dónde empezar concretamente?
Reproduce los comandos de este artículo y luego sigue el curso completo Python SQLAlchemy Pydantic: encadena las 27 lecciones en orden, con ejercicios y proyecto final.

📬 ¿Quieres recibir este tipo de guía cada semana? Suscríbete gratis — código real, cero palabrería.