Casi todas las aplicaciones reales necesitan guardar datos en algún lugar — usuarios, pedidos, artículos de un blog, lecturas de sensores, lo que sea. Tarde o temprano, tu código Python necesita comunicarse con una base de datos. Podrías escribir cadenas de SQL a mano, abrir las conexiones tú mismo y convertir las filas en objetos de Python una por una. Pero eso se vuelve repetitivo, propenso a errores y difícil de mantener muy rápido.
SQLAlchemy resuelve ese problema. Es el kit de herramientas de bases de datos más usado en Python, y está detrás de todo tipo de proyectos, desde pequeños scripts hasta grandes sistemas en producción. Esta guía explica qué es SQLAlchemy y por qué vale la pena aprenderlo, y luego te lleva paso a paso por la conexión a una base de datos, la definición de modelos, el trabajo con datos, las relaciones y consultas, el manejo de transacciones y la administración de cambios de esquema con Alembic.
Todos los ejemplos usan el estilo de SQLAlchemy 2.0, la API moderna que todo proyecto nuevo debería usar.
¿Qué Es SQLAlchemy?
SQLAlchemy es una biblioteca de Python gratuita y de código abierto para trabajar con bases de datos relacionales como SQLite, PostgreSQL, MySQL/MariaDB, Microsoft SQL Server y Oracle. Tiene dos capas principales:
- SQLAlchemy Core — una forma de escribir SQL con Python. Construyes las consultas con objetos y funciones de Python en lugar de cadenas de texto, y SQLAlchemy las convierte en el SQL correcto para tu base de datos.
- SQLAlchemy ORM (Object Relational Mapper, o mapeador objeto-relacional) — construido sobre Core. Asocia las tablas de la base de datos con clases de Python y las filas con objetos, para que puedas trabajar con un objeto
Booken lugar de una tupla de valores.
Puedes usar cualquiera de las dos capas, o ambas en el mismo proyecto. La mayoría de las aplicaciones usan el ORM para el trabajo diario y recurren a Core (o incluso a SQL puro) cuando lo necesitan.
Términos Clave
Verás estas palabras a lo largo de la guía:
- Engine (motor) — el punto de partida. Sabe cómo conectarse a tu base de datos y administra un grupo (pool) de conexiones.
- Dialecto — la parte de SQLAlchemy que habla la variante de SQL de una base de datos específica. No lo usas directamente; el engine lo elige a partir de tu URL de conexión.
- Modelo — una clase de Python que representa una tabla.
- Session (sesión) — tu espacio de trabajo para las operaciones del ORM. Lleva el control de los objetos que cargas y modificas, y guarda esos cambios en la base de datos cuando haces commit.
¿Por Qué Usar SQLAlchemy? Los Beneficios
- Escribes Python, no cadenas de SQL. Las consultas se construyen con objetos de Python, así que tu editor puede autocompletar los nombres de las columnas y detectar errores antes de ejecutar nada.
- Cambias de base de datos fácilmente. Desarrolla con SQLite en tu computadora y usa PostgreSQL en producción — normalmente cambiando solo la URL de conexión.
- Protección integrada contra la inyección SQL. SQLAlchemy envía los valores como parámetros en lugar de pegarlos dentro de la consulta, lo que bloquea uno de los ataques web más comunes. (Aprende más en Inyección SQL: Cómo Funciona y Cómo Prevenirla.)
- Menos código repetitivo. El ORM se encarga de convertir filas en objetos, de detectar los cambios y de escribir las sentencias
INSERT,UPDATEyDELETEpor ti. - Tú mantienes el control. A diferencia de otros ORM, SQLAlchemy nunca te oculta el SQL. Puedes ver cada consulta que ejecuta y escribir SQL puro cuando quieras.
- Está en todas partes. Flask, FastAPI, pandas, Alembic, Apache Airflow y muchas otras herramientas usan SQLAlchemy, así que lo que aprendas aquí te servirá en muchos lugares.
Requisitos Previos
Para seguir esta guía necesitarás:
- Python 3.10 o más reciente. Si eres nuevo en Python, empieza con Fundamentos de Python: Lo que Todo Principiante Debe Saber.
- Conocimientos básicos de SQL — saber qué son las tablas, las filas,
SELECTyJOINte ayudará mucho. - Una terminal y un editor de código.
Usaremos SQLite, que viene incluido con Python, así que no necesitas instalar un servidor de base de datos.
Paso 1: Instala SQLAlchemy
Crea una carpeta para el proyecto y un entorno virtual, y luego instala SQLAlchemy:
mkdir sqlalchemy-library
cd sqlalchemy-library
python3 -m venv .venv
source .venv/bin/activate # En Windows: .venv\Scripts\activate
pip install sqlalchemy
¿Usas uv? Ejecuta
uv inityuv add sqlalchemy. Consulta Python uv: Una Forma Más Rápida de Administrar Proyectos y Dependencias.
Revisa la versión instalada — debe ser 2.0 o más reciente:
python -c "import sqlalchemy; print(sqlalchemy.__version__)"
SQLite funciona sin configuración adicional. Para otras bases de datos también necesitas un driver:
- PostgreSQL:
pip install "psycopg[binary]" - MySQL/MariaDB:
pip install pymysql
Paso 2: Conéctate a una Base de Datos con un Engine
Crea un archivo llamado library.py. Le iremos agregando código a lo largo de esta guía. Empieza creando un engine:
from sqlalchemy import create_engine
engine = create_engine("sqlite:///library.db", echo=True)
La cadena que le pasas a create_engine() es una URL de conexión. Le indica a SQLAlchemy qué base de datos usar y cómo llegar a ella. Estos son los formatos más comunes:
sqlite:///library.db # archivo SQLite en la carpeta actual
sqlite:///:memory: # base de datos SQLite temporal en memoria
postgresql+psycopg://user:password@localhost:5432/library # PostgreSQL
mysql+pymysql://user:password@localhost:3306/library # MySQL o MariaDB
echo=True imprime cada sentencia SQL que ejecuta SQLAlchemy. Déjalo activado mientras aprendes — es la mejor forma de ver lo que pasa detrás de escena — y desactívalo en producción.
Mantén las credenciales fuera de tu código. Para bases de datos reales, guarda la URL en una variable de entorno o en un archivo .env y léela con
os.environ["DATABASE_URL"].
¿Quieres practicar con MySQL en lugar de SQLite? MySQL con Docker te muestra cómo ejecutarlo en un contenedor en pocos minutos.
Paso 3: Ejecuta SQL Puro con Core
Antes de llegar al ORM, así es como se ejecuta SQL simple a través de SQLAlchemy. Es útil para consultas rápidas, scripts y funciones de la base de datos que el ORM no cubre:
from sqlalchemy import text
with engine.connect() as conn:
result = conn.execute(
text("SELECT :greeting AS message"),
{"greeting": "Hello, SQLAlchemy!"},
)
print(result.scalar())
Fíjate en el marcador :greeting. Pasa siempre los valores como parámetros de esta forma — nunca construyas SQL con f-strings ni concatenando cadenas. SQLAlchemy envía el valor por separado de la consulta, así que ni siquiera una entrada maliciosa puede cambiar lo que hace la consulta.
El bloque with devuelve automáticamente la conexión al pool cuando terminas.
Paso 4: Define tus Modelos
Ahora sí, el ORM. Vamos a modelar una pequeña biblioteca con dos tablas: authors (autores) y books (libros). Cada autor puede escribir muchos libros, y cada libro pertenece a un solo autor — una relación clásica de uno a muchos. (Si es la primera vez que planeas tablas así, lee Diseño de Bases de Datos: Guía para Principiantes para Planearlo Bien.)
Agrega los modelos a library.py:
from datetime import datetime
from sqlalchemy import ForeignKey, String, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Author(Base):
__tablename__ = "authors"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True)
country: Mapped[str | None] = mapped_column(String(50))
books: Mapped[list["Book"]] = relationship(
back_populates="author", cascade="all, delete-orphan"
)
def __repr__(self):
return f"Author(id={self.id}, name={self.name!r})"
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
year: Mapped[int]
pages: Mapped[int | None]
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
author: Mapped["Author"] = relationship(back_populates="books")
def __repr__(self):
return f"Book(id={self.id}, title={self.title!r})"
Veamos cada parte:
Basees la clase padre de todos tus modelos. SQLAlchemy la usa para llevar el registro de cada tabla que defines.__tablename__define el nombre de la tabla en la base de datos.Mapped[...]declara una columna y su tipo de Python. SQLAlchemy elige automáticamente el tipo SQL correspondiente:intse convierte enINTEGER,strenVARCHAR,datetimeenDATETIME, etc.str | None(uOptional[str]) hace que la columna acepte valores nulos. SinNone, la columna esNOT NULL.mapped_column()agrega detalles comoprimary_key=True,unique=True, una longitud comoString(100)o un valor predeterminado.ForeignKey("authors.id")vincula cada libro con una fila de la tablaauthors.relationship()te permite moverte entre objetos relacionados desde Python:author.bookste da una lista de libros ybook.authorte da el autor.back_populatesmantiene ambos lados sincronizados.cascade="all, delete-orphan"significa que, cuando eliminas un autor, también se eliminan sus libros.
Paso 5: Crea las Tablas
Con los modelos definidos, una sola línea crea las tablas en la base de datos:
Base.metadata.create_all(engine)
Como echo=True está activado, verás las sentencias CREATE TABLE que genera SQLAlchemy. Puedes ejecutar create_all() las veces que quieras — solo crea las tablas que todavía no existen.
Nota:
create_all()nunca modifica una tabla que ya existe. Si más adelante agregas una columna a un modelo, usa Alembic (lo veremos en el Paso 11) para actualizar la base de datos.
Paso 6: Crea Registros
Para trabajar con objetos del ORM, abres una Session. Crea los objetos como cualquier otra clase de Python, agrégalos a la sesión y haz commit para guardarlos:
from sqlalchemy.orm import Session
with Session(engine) as session:
orwell = Author(name="George Orwell", country="United Kingdom")
orwell.books = [
Book(title="1984", year=1949, pages=328),
Book(title="Animal Farm", year=1945, pages=112),
]
austen = Author(name="Jane Austen", country="United Kingdom")
austen.books = [Book(title="Pride and Prejudice", year=1813, pages=432)]
marquez = Author(name="Gabriel García Márquez", country="Colombia")
marquez.books = [
Book(title="One Hundred Years of Solitude", year=1967, pages=417),
Book(title="Love in the Time of Cholera", year=1985, pages=348),
]
session.add_all([orwell, austen, marquez])
session.commit()
Nunca asignas author_id tú mismo. Como los libros están en orwell.books, SQLAlchemy inserta primero al autor, obtiene su nuevo ID y llena el author_id de cada libro automáticamente.
Hasta que llames a commit(), nada se guarda de forma permanente. Si ocurre un error antes, el bloque with cierra la sesión y los cambios se descartan.
Paso 7: Lee Registros
En SQLAlchemy 2.0, toda consulta empieza con select(). Construyes la consulta y luego la ejecutas con la sesión:
from sqlalchemy import select
with Session(engine) as session:
# Todos los libros publicados después de 1900, del más antiguo al más reciente
stmt = select(Book).where(Book.year > 1900).order_by(Book.year)
for book in session.scalars(stmt):
print(book.title, book.year)
# Una sola fila por su llave primaria
book = session.get(Book, 1)
print(book)
# Exactamente un resultado (lanza un error si hay cero o varios)
orwell = session.scalars(select(Author).where(Author.name == "George Orwell")).one()
print([b.title for b in orwell.books])
Hay varias formas de obtener los resultados de una consulta:
session.scalars(stmt)— recorre objetos del ORM (Book,Author, etc.)..all()— obtiene todos los resultados en una lista..first()— obtiene el primer resultado, oNone..one()— obtiene exactamente un resultado, o lanza un error..one_or_none()— obtiene un resultado, oNonesi no hay ninguno.session.get(Model, id)— la forma más rápida de cargar una fila por su llave primaria.
Filtrar, Ordenar y Limitar
where() acepta expresiones de Python que SQLAlchemy convierte en SQL. Estos son los operadores que más usarás:
from sqlalchemy import and_, or_
select(Book).where(Book.title == "1984") # igual a
select(Book).where(Book.year != 1949) # distinto de
select(Book).where(Book.title.like("%Time%")) # coincidencia de patrón
select(Book).where(Book.title.ilike("%time%")) # sin distinguir mayúsculas
select(Book).where(Book.year.between(1940, 1970)) # rango
select(Book).where(Book.id.in_([1, 3, 5])) # dentro de una lista
select(Book).where(Book.pages.is_(None)) # IS NULL
select(Book).where(and_(Book.year > 1900, Book.pages < 400)) # AND
select(Book).where(or_(Book.year < 1900, Book.pages > 400)) # OR
select(Book).order_by(Book.pages.desc()).limit(3) # los 3 más largos
Llamar a .where() varias veces también combina las condiciones con AND. Cada una de estas líneas es solo un objeto de consulta — pásalo a session.scalars() para ejecutarlo.
Paso 8: Actualiza y Elimina Registros
Para actualizar un registro, cárgalo, cambia sus atributos y haz commit. La sesión detecta el cambio y escribe el UPDATE por ti:
with Session(engine) as session:
book = session.scalars(select(Book).where(Book.title == "1984")).one()
book.pages = 336
session.commit()
Para actualizar muchas filas a la vez sin cargarlas en Python, usa una sentencia update():
from sqlalchemy import update
with Session(engine) as session:
session.execute(
update(Book).where(Book.year < 1900).values(pages=Book.pages + 10)
)
session.commit()
Para eliminar un registro, pásalo a session.delete():
with Session(engine) as session:
book = session.scalars(select(Book).where(Book.title == "Animal Farm")).one()
session.delete(book)
session.commit()
También existe una sentencia delete() para eliminar muchas filas a la vez, por ejemplo, session.execute(delete(Book).where(Book.year < 1800)).
Paso 9: Relaciones, Joins y Agregaciones
Como definimos una relationship(), puedes moverte entre objetos relacionados con atributos normales — author.books y book.author. Cuando necesitas filtrar o combinar datos de varias tablas en una sola consulta, usa un join. (Para repasar cómo funcionan los joins en SQL, consulta Los JOINs de SQL Explicados.)
with Session(engine) as session:
stmt = (
select(Book.title, Author.name)
.join(Book.author)
.where(Author.country == "Colombia")
)
for title, author in session.execute(stmt):
print(f"{title} by {author}")
Observa que esta consulta selecciona columnas en lugar de objetos completos, así que usa session.execute() y devuelve filas, que puedes desempacar como tuplas.
Para agregaciones como COUNT, SUM y AVG, usa func junto con group_by():
from sqlalchemy import func
stmt = (
select(
Author.name,
func.count(Book.id).label("book_count"),
func.avg(Book.pages).label("avg_pages"),
)
.join(Author.books)
.group_by(Author.name)
.order_by(func.count(Book.id).desc())
)
with Session(engine) as session:
for row in session.execute(stmt):
print(row.name, row.book_count, round(row.avg_pages))
.label() le da un nombre a cada columna calculada, para que puedas acceder a ella como row.book_count.
Evita el Problema de las Consultas N+1
De forma predeterminada, las relaciones usan carga diferida (lazy loading): author.books no se consulta hasta que accedes a él. Es cómodo, pero puede afectar el rendimiento. Mira este ciclo:
with Session(engine) as session:
for author in session.scalars(select(Author)):
print(author.name, len(author.books))
Esto ejecuta una consulta para cargar los autores y luego una consulta más por cada autor para cargar sus libros. Con 1,000 autores, son 1,001 consultas — el famoso problema N+1. Con echo=True activado, lo verás suceder.
La solución es la carga anticipada (eager loading). selectinload() carga todos los libros relacionados en una sola consulta adicional:
from sqlalchemy.orm import selectinload
with Session(engine) as session:
stmt = select(Author).options(selectinload(Author.books))
for author in session.scalars(stmt):
print(author.name, [b.title for b in author.books])
Ahora son dos consultas en total, sin importar cuántos autores tengas. Para relaciones de muchos a uno como book.author, joinedload() suele ser una mejor opción — obtiene el autor en la misma consulta con un JOIN. Las páginas lentas muchas veces se deben a consultas N+1, así que vale la pena revisarlo primero. Para saber más sobre la velocidad de las consultas en general, lee Por Qué Tu Consulta SQL Es Lenta (y Cómo Arreglarla).
Paso 10: Transacciones y Manejo de Errores
Una transacción agrupa varios cambios para que todos se completen o todos fallen juntos. La sesión siempre trabaja dentro de una transacción: commit() guarda todo lo hecho desde el último commit y rollback() lo descarta.
La forma más limpia de manejar una transacción es con session.begin(). Hace commit automáticamente al final del bloque, o rollback si se lanza una excepción:
with Session(engine) as session:
with session.begin():
allende = Author(name="Isabel Allende", country="Chile")
allende.books.append(Book(title="The House of the Spirits", year=1982, pages=448))
session.add(allende)
# Se hace commit aquí, o rollback si algo dentro del bloque lanzó un error
Cuando se viola una regla de la base de datos — por ejemplo, al insertar un valor duplicado en una columna unique — SQLAlchemy lanza un IntegrityError. Atrápalo y haz rollback para que la sesión se pueda volver a usar:
from sqlalchemy.exc import IntegrityError
with Session(engine) as session:
try:
session.add(Author(name="George Orwell"))
session.commit()
except IntegrityError:
session.rollback()
print("That author already exists.")
Paso 11: Administra los Cambios de Esquema con Alembic
Tus modelos cambiarán con el tiempo — agregarás columnas, renombrarás cosas y crearás tablas nuevas. Como create_all() no puede modificar tablas existentes, necesitas migraciones: pequeños scripts versionados que actualizan la base de datos paso a paso. Alembic, creado por el mismo autor de SQLAlchemy, es la herramienta estándar para esto.
En este punto conviene mover Base y tus modelos a su propio módulo, como models.py, para que Alembic pueda importarlos. Luego instala Alembic y crea una carpeta de migraciones:
pip install alembic
alembic init migrations
Abre alembic.ini y define la URL de tu base de datos:
sqlalchemy.url = sqlite:///library.db
Después abre migrations/env.py e indícale a Alembic dónde están tus modelos:
from models import Base
target_metadata = Base.metadata
Ahora, cada vez que cambies un modelo — por ejemplo, si agregas isbn: Mapped[str | None] = mapped_column(String(13)) a Book — deja que Alembic compare tus modelos con la base de datos y escriba la migración por ti:
alembic revision --autogenerate -m "add isbn to books"
alembic upgrade head
Abre siempre el archivo generado en migrations/versions/ y revísalo antes de ejecutar upgrade — la generación automática es muy buena, pero no detecta todo (por ejemplo, una columna renombrada parece una columna eliminada más una nueva). Si algo sale mal, deshaz la última migración con:
alembic downgrade -1
Guarda la carpeta migrations/ en tu control de versiones, para que todos los entornos — tu computadora, la de un compañero y producción — apliquen exactamente los mismos cambios.
Usar SQLAlchemy con Otras Herramientas
Flask
Flask-SQLAlchemy es una capa delgada que conecta SQLAlchemy con una aplicación Flask. Administra el engine y la sesión por ti, pero los modelos y las consultas son el mismo SQLAlchemy que acabas de aprender — db.select(), db.session.execute() y db.session.commit() funcionan exactamente igual que select() y la sesión de esta guía. Consulta Python Flask: Guía Completa para Crear y Desplegar Aplicaciones Web para ver un ejemplo completo.
pandas
pandas puede leer los resultados de una consulta directamente en un DataFrame usando tu engine de SQLAlchemy — ideal para análisis y reportes:
import pandas as pd
df = pd.read_sql(select(Book.title, Book.year, Book.pages), engine)
print(df.describe())
A partir de ahí, puedes limpiar y analizar los datos como siempre. (Consulta Limpieza de Datos con Python Pandas: De los Datos en Bruto al Análisis.)
Buenas Prácticas con SQLAlchemy
- Crea un solo engine por aplicación y reutilízalo. El engine administra un pool de conexiones; crear uno nuevo en cada petición desperdicia recursos.
- Mantén las sesiones cortas. Abre una sesión para cada unidad de trabajo — una petición web, una tarea, un paso de un script — y ciérrala al terminar. El patrón
with Session(engine) as session:lo hace por ti. - Usa el estilo 2.0. Escribe las consultas con
select()ysession.scalars(). Los tutoriales más antiguos usansession.query(...), que todavía funciona pero ahora se considera heredado (legacy). - Nunca construyas SQL con f-strings. Usa el ORM, las expresiones de Core o
text()con parámetros. - Vigila las consultas N+1. Activa
echo=Truedurante el desarrollo y usaselectinload()ojoinedload()cuando recorras relaciones en un ciclo. - Usa Alembic desde el principio en cualquier base de datos con datos que te importen.
- Agrega índices a las columnas que usas con frecuencia para filtrar o hacer joins, con
mapped_column(index=True). - Guarda tu URL de conexión en una variable de entorno, nunca en tu código ni en tu repositorio.
Solución de Errores Comunes en SQLAlchemy
sqlite3.OperationalError: no such table. La tabla todavía no se ha creado. Asegúrate de que Base.metadata.create_all(engine) se ejecute después de definir (o importar) tus modelos, y de que te conectas al mismo archivo de base de datos. Una ruta relativa como sqlite:///library.db depende de la carpeta desde la que ejecutas el script.
IntegrityError: UNIQUE constraint failed. Insertaste un valor que ya existe en una columna unique. En esta guía, ejecutar dos veces el script del Paso 6 provoca este error, porque los nombres de los autores son únicos. Borra library.db para empezar de nuevo, o revisa si la fila ya existe antes de insertarla.
PendingRollbackError. Un error anterior dejó la transacción de la sesión en un estado fallido. Llama a session.rollback() después de atrapar una excepción y antes de volver a usar la sesión.
DetachedInstanceError. Accediste a una relación con carga diferida (como author.books) después de cerrar la sesión. Accede a los datos mientras la sesión sigue abierta, o cárgalos por adelantado con selectinload().
NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres. El esquema de la URL debe ser postgresql:// (o postgresql+psycopg://), no postgres://. Algunos proveedores de hosting te dan la forma antigua, así que reemplázala antes de llamar a create_engine().
ModuleNotFoundError: No module named 'psycopg' (o pymysql). SQLAlchemy no incluye los drivers de las bases de datos. Instala el driver que corresponde a tu URL de conexión.
MultipleResultsFound o NoResultFound. Llamaste a .one(), pero la consulta devolvió varias filas o ninguna. Usa .first() o .one_or_none() si es posible que haya cero o varios resultados.
Conclusión
SQLAlchemy te da lo mejor de dos mundos: la productividad de trabajar con objetos de Python y todo el poder de SQL cuando lo necesitas. En esta guía te conectaste a una base de datos con un engine, ejecutaste SQL puro de forma segura, definiste modelos y relaciones, creaste, leíste, actualizaste y eliminaste registros, escribiste joins y agregaciones, resolviste el problema N+1, manejaste transacciones y errores, y configuraste Alembic para los cambios de esquema.
El mejor siguiente paso es usarlo en un proyecto real. Toma un script que escriba en archivos CSV o que construya cadenas de SQL a mano, y reescríbelo con modelos de SQLAlchemy. Cuando veas lo limpio que queda el código, vas a querer usarlo en todos tus proyectos que trabajen con una base de datos.
