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 Book en 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, UPDATE y DELETE por 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:

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 init y uv 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:

  • Base es 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: int se convierte en INTEGER, str en VARCHAR, datetime en DATETIME, etc.
  • str | None (u Optional[str]) hace que la columna acepte valores nulos. Sin None, la columna es NOT NULL.
  • mapped_column() agrega detalles como primary_key=True, unique=True, una longitud como String(100) o un valor predeterminado.
  • ForeignKey("authors.id") vincula cada libro con una fila de la tabla authors.
  • relationship() te permite moverte entre objetos relacionados desde Python: author.books te da una lista de libros y book.author te da el autor. back_populates mantiene 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, o None.
  • .one() — obtiene exactamente un resultado, o lanza un error.
  • .one_or_none() — obtiene un resultado, o None si 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() y session.scalars(). Los tutoriales más antiguos usan session.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=True durante el desarrollo y usa selectinload() o joinedload() 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.