DB-API 2.0 (definida en el PEP 249) es la especificación que dicta qué métodos y comportamientos debe exponer cualquier adaptador de base de datos en Python. psycopg2 para PostgreSQL, mysql-connector-python para MySQL, sqlite3 de la stdlib: todos implementan la misma interfaz. Eso significa que el código que escribes hoy con SQLite puede migrar a Postgres cambiando prácticamente solo la línea de conexión.
El flujo del estándar es deliberadamente simple: obtienes una conexión (Connection), de ella creas un cursor (Cursor), y con el cursor ejecutas sentencias y recuperas resultados. La separación entre conexión y cursor no es arbitraria — la conexión modela el canal hacia la base de datos (y la transacción activa), mientras que el cursor modela el estado de una consulta individual. Puedes tener varios cursores sobre la misma conexión ejecutando cosas distintas.
sqlite3 viene en la stdlib desde Python 2.5 y envuelve SQLite 3, una base de datos embebida que vive enteramente en un archivo. No necesita servidor, no necesita configuración. Para cargas moderadas, aplicaciones de escritorio, prototipos que luego escalan, o almacenamiento local en tools de CLI, es perfectamente legítima en producción.
El punto donde más gente tropieza es la parametrización de consultas. Jamás construyas una sentencia SQL concatenando strings con datos del usuario — eso abre la puerta a SQL injection de libro. DB-API 2.0 define marcadores de posición para esto; SQLite usa ? (otros adaptadores usan %s o :nombre, pero el mecanismo es el mismo). Pasas los valores en una tupla separada y el driver se encarga de escapar todo correctamente.
El otro mecanismo clave es el context manager sobre la conexión. Cuando usas with conn:, Python hace commit automático si el bloque termina sin excepciones, y rollback si se lanza alguna. Ojo: esto no cierra la conexión — solo gestiona la transacción. Para cerrar la conexión también necesitas conn.close() o un segundo context manager si usas sqlite3.connect() como tal.
import sqlite3
from pathlib import Path
DB_PATH = Path("library.db")
def init_db(conn: sqlite3.Connection) -> None:
with conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
year INTEGER
)
""")
def add_book(conn: sqlite3.Connection, title: str, author: str, year: int | None = None) -> int:
with conn:
cursor = conn.execute(
"INSERT INTO books (title, author, year) VALUES (?, ?, ?)",
(title, author, year), # nunca interpolar — siempre parámetros
)
return cursor.lastrowid # id asignado por la base de datos
def find_by_author(conn: sqlite3.Connection, author: str) -> list[dict]:
conn.row_factory = sqlite3.Row # filas accesibles por nombre de columna
cursor = conn.execute(
"SELECT id, title, year FROM books WHERE author = ? ORDER BY year",
(author,),
)
# fetchall() trae todo en memoria; para millones de filas usa fetchmany() o itera
return [dict(row) for row in cursor.fetchall()]
def delete_book(conn: sqlite3.Connection, book_id: int) -> bool:
with conn:
cursor = conn.execute("DELETE FROM books WHERE id = ?", (book_id,))
return cursor.rowcount > 0 # rowcount indica cuántas filas afectó la operación
def main() -> None:
with sqlite3.connect(DB_PATH) as conn:
# sqlite3.connect como context manager cierra la conexión al salir
init_db(conn)
rowid = add_book(conn, "Fluent Python", "Luciano Ramalho", 2022)
print(f"Inserted id={rowid}")
add_book(conn, "Python Cookbook", "David Beazley", 2013)
add_book(conn, "Python Tricks", "Dan Bader", 2017)
books = find_by_author(conn, "Luciano Ramalho")
for book in books:
print(book)
removed = delete_book(conn, rowid)
print(f"Deleted: {removed}")
if __name__ == "__main__":
main()
Fíjate en cómo init_db usa conn.execute() directamente en lugar de crear un cursor explícito. Connection.execute() es un shortcut de SQLite (no forma parte del PEP 249 estricto, pero está disponible en la mayoría de adaptadores modernos) que crea un cursor implícito y lo devuelve. Para consultas únicas, es más limpio; para bucles donde reutilizas el cursor, créalo explícitamente.
El patrón with conn: en add_book y delete_book es la gestión de transacciones recomendada. Si conn.execute() lanza, se hace rollback automático. Pero nota que la función recibe la conexión como parámetro — no la abre ella misma. Esto es importante: quien crea la conexión es quien controla el ciclo de vida de la transacción. Si add_book abriera su propia conexión interna, perderías la posibilidad de agregar varios libros dentro de una sola transacción atómica desde el código llamador.
sqlite3.Row como row_factory transforma cada fila de tupla posicional a un objeto que soporta acceso por índice y por nombre de columna. El dict(row) al final es opcional — a veces quieres mantener Row porque es lazy y más eficiente en memoria cuando no accedes a todos los campos.
cursor.rowcount es el mecanismo estándar del PEP 249 para saber cuántas filas afectó un UPDATE, DELETE o INSERT. Para SELECT su valor no está definido de forma confiable, así que no lo uses ahí.
Errores que debes conocer
Error: Interpolar variables directamente en el string de la consulta con f-strings o % de strings, creyendo que es equivalente a usar parámetros.
# ❌ Wrong
user_input = "1 OR 1=1"
cursor.execute(f"SELECT * FROM books WHERE id = {user_input}")
# ✅ Right
user_input = "1 OR 1=1"
cursor.execute("SELECT * FROM books WHERE id = ?", (user_input,))
Con el marcador ?, el driver trata el valor como dato puro, no como SQL; la inyección queda imposibilitada a nivel de protocolo.
Error: Olvidar que un parámetro único se pasa como tupla de un elemento, no como valor suelto.
# ❌ Wrong — itera sobre los caracteres del string
cursor.execute("SELECT * FROM books WHERE author = ?", "Ramalho")
# ✅ Right
cursor.execute("SELECT * FROM books WHERE author = ?", ("Ramalho",))
DB-API 2.0 espera una secuencia de parámetros; pasar un string lo convierte implícitamente en secuencia de caracteres, produciendo un error o resultados incorrectos.
Error: Asumir que with sqlite3.connect(path) as conn: hace rollback si el bloque lanza una excepción, sin usar un with conn: explícito para las operaciones.
# ❌ Wrong — el INSERT puede quedar sin commit o sin rollback predecible
with sqlite3.connect("db.sqlite") as conn:
conn.execute("INSERT INTO books VALUES (?, ?, ?, ?)", data)
raise ValueError("algo salió mal")
# ✅ Right — la transacción está explícitamente delimitada
with sqlite3.connect("db.sqlite") as conn:
with conn: # este with gestiona la transacción
conn.execute("INSERT INTO books VALUES (?, ?, ?, ?)", data)
raise ValueError("algo salió mal") # → rollback automático
sqlite3.connect() como context manager solo garantiza que se llama close() al salir; el with conn: interno es el que realmente delimita la transacción y garantiza commit/rollback.
N° 181