Skip to content

Tutorial de Python SQLite3: Guía completa de la base de datos SQLite en Python

Updated on

Necesitas almacenar datos estructurados en tu aplicación Python. Quizá sean ajustes de usuario, lecturas de sensores, entradas de logs o una lista de tareas. Recurres a un archivo CSV, pero los datos crecen y las consultas se vuelven dolorosas. Consideras PostgreSQL o MySQL, pero configurar un servidor de base de datos para una herramienta local o un prototipo se siente como exceso. Solo necesitas una forma confiable de almacenar, consultar y actualizar datos estructurados sin sobrecarga.

Este es exactamente el problema que resuelve SQLite. SQLite es un motor de base de datos autocontenido y sin servidor que guarda todo en un solo archivo. Python incluye el módulo sqlite3 en la biblioteca estándar, así que no hay nada que instalar. Obtienes soporte completo de SQL, transacciones ACID y la capacidad de manejar conjuntos de datos de hasta 281 terabytes, todo sin ejecutar un proceso de servidor ni administrar archivos de configuración.

Esta guía recorre todo lo que necesitas para usar SQLite de forma efectiva en Python: desde crear tu primera base de datos hasta consultas avanzadas, integración con pandas, ajustes de rendimiento y un proyecto completo del mundo real.

📚

¿Qué es SQLite y por qué usarlo?

SQLite es un motor de base de datos relacional embebido. A diferencia de PostgreSQL o MySQL, no se ejecuta como un proceso de servidor separado. La base de datos completa vive en un único archivo en disco (o en memoria). El módulo sqlite3 de la biblioteca estándar de Python ofrece una interfaz compatible con DB-API 2.0 para SQLite.

Características clave de SQLite:

  • Cero configuración: Sin configuración de servidor, sin usuarios, sin permisos que gestionar
  • Sin servidor: El motor de base de datos se ejecuta dentro del proceso de tu aplicación
  • Archivo único: Toda la base de datos (tablas, índices, datos) es un solo archivo .db
  • Multiplataforma: El archivo de base de datos funciona en cualquier SO sin conversión
  • Compatible con ACID: Soporte completo de transacciones con commit y rollback
  • Ligero: La librería pesa ~750 KB, más pequeña que la mayoría de imágenes

Cuándo usar SQLite

Caso de usoSQLitePostgreSQL/MySQL
Apps de escritorio/móvilMejor opciónExcesivo
PrototipadoMejor opciónMás lento de montar
Dispositivos embebidos / IoTMejor opciónNo viable
Unit testingMejor opciónRequiere servidor de prueba
Scripts de análisis de datosBuena opciónSobrecarga innecesaria
Web app con < 100K visitas diariasFunciona bienMás escalable
Web app de alta concurrenciaNo idealMejor opción
Varios clientes con muchas escriturasNo idealMejor opción

SQLite cubre la mayoría de aplicaciones que no requieren múltiples conexiones de escritura simultáneas. Android e iOS usan SQLite como base de datos local por defecto. Firefox, Chrome y macOS lo usan internamente. Es el motor de base de datos más desplegado del mundo.

Conectarse a una base de datos

La función sqlite3.connect() crea una conexión a un archivo de base de datos. Si el archivo no existe, SQLite lo crea automáticamente.

import sqlite3
 
# Connect to a database file (creates it if it doesn't exist)
conn = sqlite3.connect("myapp.db")
 
# Create a cursor to execute SQL statements
cursor = conn.cursor()
 
# Always close the connection when done
conn.close()

Bases de datos en memoria

Usa la cadena especial :memory: para crear una base de datos que existe solo en RAM. Es ideal para pruebas y procesamiento temporal de datos.

import sqlite3
 
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
 
cursor.execute("CREATE TABLE test (id INTEGER, value TEXT)")
cursor.execute("INSERT INTO test VALUES (1, 'hello')")
cursor.execute("SELECT * FROM test")
print(cursor.fetchone())  # (1, 'hello')
 
conn.close()  # Database is gone after this

Usar context managers

El patrón recomendado usa un context manager (sentencia with) para asegurar que la conexión se gestione correctamente incluso si ocurre un error. Las conexiones SQLite también hacen auto-commit si todo sale bien y auto-rollback si hay una excepción dentro del bloque with.

import sqlite3
 
# Connection as context manager handles commit/rollback
with sqlite3.connect("myapp.db") as conn:
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
    # Auto-commits if no exception
    # Auto-rolls back if an exception occurs
 
# Note: the connection is NOT closed by 'with' -- it just commits/rolls back
# For full cleanup, close explicitly or use a wrapper:
conn.close()

Una función helper limpia que maneja commit/rollback y también el cierre:

import sqlite3
from contextlib import contextmanager
 
@contextmanager
def get_db(db_path="myapp.db"):
    """Context manager that commits on success, rolls back on error, and always closes."""
    conn = sqlite3.connect(db_path)
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()
 
# Usage
with get_db() as conn:
    conn.execute("INSERT INTO users (name) VALUES (?)", ("Bob",))

Crear tablas

Usa CREATE TABLE para definir el esquema. SQLite soporta un sistema de tipos simplificado con cinco clases de almacenamiento.

Tipos de datos en SQLite

Clase de almacenamientoDescripciónEquivalente en Python
NULLValor nuloNone
INTEGEREntero con signo (1, 2, 3, 4, 6 u 8 bytes)int
REALPunto flotante (float IEEE de 8 bytes)float
TEXTCadena UTF-8 o UTF-16str
BLOBDatos binarios, almacenados exactamente como se ingresanbytes

SQLite usa tipado dinámico. Puedes almacenar cualquier tipo en cualquier columna independientemente del tipo declarado. El tipo declarado es una pista, no una restricción. Esto es diferente a PostgreSQL y MySQL.

Crear una tabla con restricciones

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS employees (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL,
        department TEXT DEFAULT 'General',
        salary REAL CHECK(salary > 0),
        hire_date TEXT NOT NULL,
        is_active INTEGER DEFAULT 1
    )
""")
 
conn.commit()
conn.close()

Restricciones clave:

  • PRIMARY KEY: Identificador único para cada fila. INTEGER PRIMARY KEY es un alias del rowid interno.
  • AUTOINCREMENT: Garantiza que el rowid siempre aumente (nunca reutiliza IDs borrados).
  • NOT NULL: La columna no puede contener valores NULL.
  • UNIQUE: No puede haber dos filas con el mismo valor en esta columna.
  • DEFAULT: Valor por defecto si no se proporciona ninguno durante el INSERT.
  • CHECK: Valida datos contra una condición.

Crear múltiples tablas relacionadas

import sqlite3
 
conn = sqlite3.connect("school.db")
cursor = conn.cursor()
 
# Enable foreign key enforcement (off by default in SQLite)
cursor.execute("PRAGMA foreign_keys = ON")
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS students (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE
    )
""")
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS courses (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        credits INTEGER DEFAULT 3
    )
""")
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS enrollments (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER NOT NULL,
        course_id INTEGER NOT NULL,
        grade TEXT,
        enrolled_date TEXT DEFAULT CURRENT_DATE,
        FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
        FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
        UNIQUE(student_id, course_id)
    )
""")
 
conn.commit()
conn.close()

Operaciones CRUD

CRUD significa Create, Read, Update, Delete: las cuatro operaciones básicas del almacenamiento persistente.

INSERT: agregar datos

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Insert a single row
cursor.execute(
    "INSERT INTO employees (name, email, department, salary, hire_date) VALUES (?, ?, ?, ?, ?)",
    ("Alice Johnson", "alice@company.com", "Engineering", 95000, "2024-01-15")
)
 
# Insert multiple rows with executemany
employees = [
    ("Bob Smith", "bob@company.com", "Marketing", 72000, "2024-03-01"),
    ("Carol White", "carol@company.com", "Engineering", 98000, "2023-11-20"),
    ("David Brown", "david@company.com", "Sales", 68000, "2024-06-10"),
    ("Eve Davis", "eve@company.com", "Engineering", 105000, "2023-08-05"),
]
 
cursor.executemany(
    "INSERT INTO employees (name, email, department, salary, hire_date) VALUES (?, ?, ?, ?, ?)",
    employees
)
 
# Get the last inserted row ID
print(f"Last inserted ID: {cursor.lastrowid}")
 
# Get the number of rows affected
print(f"Rows inserted: {cursor.rowcount}")
 
conn.commit()
conn.close()

INSERT OR REPLACE (Upsert)

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Insert or update if email already exists
cursor.execute("""
    INSERT INTO employees (name, email, department, salary, hire_date)
    VALUES (?, ?, ?, ?, ?)
    ON CONFLICT(email) DO UPDATE SET
        name = excluded.name,
        salary = excluded.salary
""", ("Alice Johnson", "alice@company.com", "Engineering", 100000, "2024-01-15"))
 
conn.commit()
conn.close()

SELECT: leer datos

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Select all rows
cursor.execute("SELECT * FROM employees")
all_rows = cursor.fetchall()
for row in all_rows:
    print(row)  # Returns tuples by default
 
# Select specific columns with a condition
cursor.execute(
    "SELECT name, salary FROM employees WHERE department = ?",
    ("Engineering",)
)
engineers = cursor.fetchall()
for name, salary in engineers:
    print(f"{name}: ${salary:,.0f}")
 
# Select with ordering and limit
cursor.execute("""
    SELECT name, salary FROM employees
    ORDER BY salary DESC
    LIMIT 3
""")
top_earners = cursor.fetchall()
print("Top 3 earners:", top_earners)
 
conn.close()

Obtener resultados como diccionarios

Por defecto, fetchall() devuelve tuplas. Para obtener diccionarios (nombre de columna como clave), usa sqlite3.Row:

import sqlite3
 
conn = sqlite3.connect("myapp.db")
conn.row_factory = sqlite3.Row  # Set row factory
 
cursor = conn.cursor()
cursor.execute("SELECT * FROM employees WHERE department = ?", ("Engineering",))
 
for row in cursor.fetchall():
    print(dict(row))  # Convert Row to dict
    print(row["name"], row["salary"])  # Access by column name
 
conn.close()

UPDATE: modificar datos

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Update a single field
cursor.execute(
    "UPDATE employees SET salary = ? WHERE email = ?",
    (110000, "alice@company.com")
)
print(f"Rows updated: {cursor.rowcount}")
 
# Update with a calculation
cursor.execute("""
    UPDATE employees
    SET salary = salary * 1.10
    WHERE department = 'Engineering' AND salary < 100000
""")
print(f"Engineers with raises: {cursor.rowcount}")
 
# Conditional update
cursor.execute("""
    UPDATE employees
    SET is_active = 0
    WHERE hire_date < '2023-01-01'
""")
 
conn.commit()
conn.close()

DELETE: eliminar datos

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Delete specific rows
cursor.execute("DELETE FROM employees WHERE is_active = 0")
print(f"Rows deleted: {cursor.rowcount}")
 
# Delete with a subquery
cursor.execute("""
    DELETE FROM enrollments
    WHERE student_id NOT IN (SELECT id FROM students)
""")
 
# Delete all rows (truncate equivalent)
cursor.execute("DELETE FROM employees")
 
conn.commit()
conn.close()

Consultas parametrizadas y prevención de SQL injection

Nunca construyas consultas SQL con formateo de strings o f-strings. Usa siempre consultas parametrizadas para prevenir ataques de SQL injection.

El problema: SQL injection

import sqlite3
 
# DANGEROUS: Never do this
user_input = "'; DROP TABLE employees; --"
query = f"SELECT * FROM employees WHERE name = '{user_input}'"
# This would execute: SELECT * FROM employees WHERE name = ''; DROP TABLE employees; --'

La solución: consultas parametrizadas

SQLite3 soporta dos estilos de placeholders:

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Style 1: Question mark placeholders (positional)
cursor.execute(
    "SELECT * FROM employees WHERE department = ? AND salary > ?",
    ("Engineering", 90000)
)
 
# Style 2: Named placeholders
cursor.execute(
    "SELECT * FROM employees WHERE department = :dept AND salary > :min_salary",
    {"dept": "Engineering", "min_salary": 90000}
)
 
# Named placeholders are clearer with many parameters
cursor.execute("""
    INSERT INTO employees (name, email, department, salary, hire_date)
    VALUES (:name, :email, :dept, :salary, :date)
""", {
    "name": "Frank Green",
    "email": "frank@company.com",
    "dept": "Engineering",
    "salary": 92000,
    "date": "2025-09-01"
})
 
conn.commit()
conn.close()

Las consultas parametrizadas gestionan el escape y el quoting automáticamente. El motor de base de datos trata los valores como datos, nunca como código SQL. Esto elimina el SQL injection sin importar el contenido del input.

Cláusulas WHERE dinámicas

A veces necesitas construir consultas dinámicamente, por ejemplo cuando los filtros son opcionales. Aquí tienes un patrón seguro:

import sqlite3
 
def search_employees(department=None, min_salary=None, is_active=None):
    conn = sqlite3.connect("myapp.db")
    cursor = conn.cursor()
 
    conditions = []
    params = []
 
    if department is not None:
        conditions.append("department = ?")
        params.append(department)
 
    if min_salary is not None:
        conditions.append("salary >= ?")
        params.append(min_salary)
 
    if is_active is not None:
        conditions.append("is_active = ?")
        params.append(is_active)
 
    query = "SELECT * FROM employees"
    if conditions:
        query += " WHERE " + " AND ".join(conditions)
 
    cursor.execute(query, params)
    results = cursor.fetchall()
    conn.close()
    return results
 
# Usage
engineers = search_employees(department="Engineering", min_salary=90000)
active_staff = search_employees(is_active=1)
everyone = search_employees()  # No filters

Trabajar con transacciones

Una transacción agrupa múltiples operaciones SQL en una unidad atómica. O bien todas las operaciones tienen éxito (commit) o ninguna tiene efecto (rollback).

Control manual de transacciones

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
try:
    # Transfer money between accounts
    cursor.execute("UPDATE accounts SET balance = balance - 500 WHERE id = 1")
    cursor.execute("UPDATE accounts SET balance = balance + 500 WHERE id = 2")
 
    # Verify no negative balance
    cursor.execute("SELECT balance FROM accounts WHERE id = 1")
    balance = cursor.fetchone()[0]
    if balance < 0:
        raise ValueError("Insufficient funds")
 
    conn.commit()  # All good, save changes
    print("Transfer complete")
 
except Exception as e:
    conn.rollback()  # Something went wrong, undo everything
    print(f"Transfer failed: {e}")
 
finally:
    conn.close()

Autocommit y niveles de aislamiento

Por defecto, sqlite3 opera en modo de “transacción diferida” (deferred). Puedes cambiar este comportamiento:

import sqlite3
 
# Default behavior: transactions are deferred
conn = sqlite3.connect("myapp.db")
 
# Autocommit mode (each statement is its own transaction)
conn = sqlite3.connect("myapp.db", isolation_level=None)
 
# Immediate locking (prevents concurrent write conflicts)
conn = sqlite3.connect("myapp.db")
conn.execute("BEGIN IMMEDIATE")
# ... your operations ...
conn.commit()

Savepoints para rollbacks parciales

Los savepoints permiten hacer rollback de una parte de una transacción sin deshacer todo:

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
try:
    cursor.execute("INSERT INTO orders (customer_id, total) VALUES (1, 100)")
 
    # Start a savepoint
    cursor.execute("SAVEPOINT order_items")
 
    try:
        cursor.execute("INSERT INTO order_items (order_id, product, qty) VALUES (1, 'Widget', 5)")
        cursor.execute("INSERT INTO order_items (order_id, product, qty) VALUES (1, 'Gadget', -1)")
        # This might fail due to CHECK constraint
        cursor.execute("RELEASE SAVEPOINT order_items")
    except sqlite3.IntegrityError:
        cursor.execute("ROLLBACK TO SAVEPOINT order_items")
        print("Some items failed, but order was still created")
 
    conn.commit()
except Exception:
    conn.rollback()
finally:
    conn.close()

Obtener datos: fetchone, fetchall, fetchmany

El objeto cursor ofrece tres métodos para recuperar resultados de una consulta.

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
cursor.execute("SELECT * FROM employees ORDER BY salary DESC")
 
# fetchone: Get the next single row (or None if no more rows)
top_earner = cursor.fetchone()
print(f"Top earner: {top_earner}")
 
# fetchmany: Get the next N rows
next_three = cursor.fetchmany(3)
print(f"Next 3: {next_three}")
 
# fetchall: Get all remaining rows
remaining = cursor.fetchall()
print(f"Remaining: {len(remaining)} rows")
 
conn.close()

Iterar sobre resultados de forma eficiente

Para conjuntos de resultados grandes, itera directamente sobre el cursor en vez de cargar todo en memoria:

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
cursor.execute("SELECT name, salary FROM employees")
 
# Memory-efficient: processes one row at a time
for name, salary in cursor:
    print(f"{name}: ${salary:,.0f}")
 
conn.close()

fetchmany con procesamiento por lotes

Al procesar millones de filas, fetchmany() en un bucle te da control sobre el uso de memoria:

import sqlite3
 
conn = sqlite3.connect("large_data.db")
cursor = conn.cursor()
 
cursor.execute("SELECT * FROM sensor_readings")
 
batch_size = 1000
while True:
    rows = cursor.fetchmany(batch_size)
    if not rows:
        break
    # Process batch
    for row in rows:
        pass  # your processing logic
 
conn.close()

Consultas avanzadas

JOINs

import sqlite3
 
conn = sqlite3.connect("school.db")
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
 
# INNER JOIN: Students with their enrolled courses
cursor.execute("""
    SELECT s.name AS student, c.title AS course, e.grade
    FROM enrollments e
    INNER JOIN students s ON e.student_id = s.id
    INNER JOIN courses c ON e.course_id = c.id
    ORDER BY s.name, c.title
""")
 
for row in cursor.fetchall():
    print(f"{row['student']} - {row['course']}: {row['grade'] or 'In Progress'}")
 
# LEFT JOIN: All students, even those not enrolled
cursor.execute("""
    SELECT s.name, COUNT(e.id) AS course_count
    FROM students s
    LEFT JOIN enrollments e ON s.id = e.student_id
    GROUP BY s.id
    ORDER BY course_count DESC
""")
 
for row in cursor.fetchall():
    print(f"{row['name']}: {row['course_count']} courses")
 
conn.close()

Agregación

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Department statistics
cursor.execute("""
    SELECT
        department,
        COUNT(*) AS headcount,
        ROUND(AVG(salary), 2) AS avg_salary,
        MIN(salary) AS min_salary,
        MAX(salary) AS max_salary,
        SUM(salary) AS total_payroll
    FROM employees
    WHERE is_active = 1
    GROUP BY department
    HAVING COUNT(*) >= 2
    ORDER BY avg_salary DESC
""")
 
print(f"{'Department':<15} {'Count':<8} {'Avg Salary':<12} {'Min':<10} {'Max':<10}")
print("-" * 55)
for row in cursor.fetchall():
    dept, count, avg_sal, min_sal, max_sal, total = row
    print(f"{dept:<15} {count:<8} ${avg_sal:<11,.0f} ${min_sal:<9,.0f} ${max_sal:<9,.0f}")
 
conn.close()

Subconsultas

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Employees earning above their department average
cursor.execute("""
    SELECT name, department, salary
    FROM employees e
    WHERE salary > (
        SELECT AVG(salary) FROM employees
        WHERE department = e.department
    )
    ORDER BY department, salary DESC
""")
 
for name, dept, salary in cursor.fetchall():
    print(f"{name} ({dept}): ${salary:,.0f}")
 
# Most recent hire in each department
cursor.execute("""
    SELECT name, department, hire_date
    FROM employees
    WHERE (department, hire_date) IN (
        SELECT department, MAX(hire_date)
        FROM employees
        GROUP BY department
    )
""")
 
for row in cursor.fetchall():
    print(row)
 
conn.close()

Funciones de ventana (SQLite 3.25+)

SQLite soporta funciones de ventana para totales acumulados, rankings y otras consultas analíticas:

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Rank employees by salary within each department
cursor.execute("""
    SELECT
        name,
        department,
        salary,
        RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
        RANK() OVER (ORDER BY salary DESC) AS overall_rank
    FROM employees
    WHERE is_active = 1
""")
 
for name, dept, salary, dept_rank, overall_rank in cursor.fetchall():
    print(f"#{overall_rank} overall, #{dept_rank} in {dept}: {name} (${salary:,.0f})")
 
# Running total of payroll by hire date
cursor.execute("""
    SELECT
        name,
        hire_date,
        salary,
        SUM(salary) OVER (ORDER BY hire_date) AS running_total
    FROM employees
    ORDER BY hire_date
""")
 
for name, date, salary, running in cursor.fetchall():
    print(f"{date} | {name}: ${salary:,.0f} | Running total: ${running:,.0f}")
 
conn.close()

SQLite con pandas

pandas tiene soporte integrado para SQLite mediante read_sql() y to_sql(). Esto facilita mover datos entre DataFrames y bases de datos SQLite.

Leer resultados SQL en un DataFrame

import sqlite3
import pandas as pd
 
conn = sqlite3.connect("myapp.db")
 
# Read a SQL query into a DataFrame
df = pd.read_sql("SELECT * FROM employees WHERE is_active = 1", conn)
print(df.head())
print(df.describe())
 
# Parameterized query with pandas
df = pd.read_sql(
    "SELECT * FROM employees WHERE department = ? AND salary > ?",
    conn,
    params=("Engineering", 90000)
)
 
# Read an entire table
df = pd.read_sql("SELECT * FROM employees", conn, index_col="id")
 
conn.close()

Escribir un DataFrame en SQLite

import sqlite3
import pandas as pd
 
# Create a sample DataFrame
df = pd.DataFrame({
    "product": ["Laptop", "Phone", "Tablet", "Monitor", "Keyboard"],
    "price": [999.99, 699.99, 449.99, 349.99, 79.99],
    "stock": [50, 200, 75, 30, 500],
    "category": ["Electronics", "Electronics", "Electronics", "Peripherals", "Peripherals"]
})
 
conn = sqlite3.connect("store.db")
 
# Write DataFrame to a new table
df.to_sql("products", conn, if_exists="replace", index=False)
 
# Append to an existing table
new_products = pd.DataFrame({
    "product": ["Mouse", "Webcam"],
    "price": [49.99, 89.99],
    "stock": [300, 100],
    "category": ["Peripherals", "Peripherals"]
})
new_products.to_sql("products", conn, if_exists="append", index=False)
 
# Verify
result = pd.read_sql("SELECT * FROM products", conn)
print(result)
 
conn.close()

Flujo round-trip: CSV → SQLite → análisis

import sqlite3
import pandas as pd
 
# Step 1: Load CSV into SQLite
df = pd.read_csv("sales_data.csv")
conn = sqlite3.connect("analytics.db")
df.to_sql("sales", conn, if_exists="replace", index=False)
 
# Step 2: Run SQL analytics
summary = pd.read_sql("""
    SELECT
        region,
        strftime('%Y-%m', sale_date) AS month,
        COUNT(*) AS transactions,
        SUM(amount) AS revenue,
        AVG(amount) AS avg_transaction
    FROM sales
    GROUP BY region, month
    ORDER BY month, revenue DESC
""", conn)
 
print(summary)
 
# Step 3: Export results
summary.to_csv("monthly_summary.csv", index=False)
 
conn.close()

Una vez que tengas los resultados de tu consulta en un DataFrame, puedes visualizarlos directamente usando PyGWalker (opens in a new tab). PyGWalker convierte cualquier pandas DataFrame en una interfaz de visualización interactiva similar a Tableau, permitiéndote arrastrar y soltar campos para crear gráficos sin escribir código de plotting:

import pygwalker as pyg
 
# Turn your SQL query results into an interactive visualization
walker = pyg.walk(summary)

Para un flujo todavía más fluido, RunCell (opens in a new tab) ofrece un entorno Jupyter con IA donde puedes escribir consultas SQL, obtener depuración asistida por IA y visualizar resultados al instante, todo en un solo notebook.

Manejo de errores

El módulo sqlite3 define varios tipos de excepciones. Capturar excepciones específicas lleva a mejores mensajes de error y estrategias de recuperación.

import sqlite3
 
def safe_insert(db_path, name, email, salary):
    """Insert an employee with proper error handling."""
    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute(
            "INSERT INTO employees (name, email, salary, hire_date) VALUES (?, ?, ?, date('now'))",
            (name, email, salary)
        )
        conn.commit()
        return cursor.lastrowid
 
    except sqlite3.IntegrityError as e:
        # Unique constraint violated (duplicate email, NOT NULL violated, etc.)
        print(f"Data integrity error: {e}")
        return None
 
    except sqlite3.OperationalError as e:
        # Table doesn't exist, SQL syntax error, database locked, etc.
        print(f"Operational error: {e}")
        return None
 
    except sqlite3.DatabaseError as e:
        # Corrupted database, disk full, etc.
        print(f"Database error: {e}")
        return None
 
    except sqlite3.ProgrammingError as e:
        # Using a closed cursor, wrong number of parameters, etc.
        print(f"Programming error: {e}")
        return None
 
    finally:
        if conn:
            conn.close()

Jerarquía de excepciones de SQLite3

ExcepciónPadreCausas comunes
sqlite3.WarningExceptionProblemas no fatales
sqlite3.ErrorExceptionClase base para todos los errores de sqlite3
sqlite3.InterfaceErrorErrorMal uso de la interfaz DB-API
sqlite3.DatabaseErrorErrorErrores relacionados con la base de datos
sqlite3.DataErrorDatabaseErrorProblemas de procesamiento de datos
sqlite3.OperationalErrorDatabaseErrorBase bloqueada, tabla ausente, error SQL
sqlite3.IntegrityErrorDatabaseErrorViolaciones de UNIQUE, NOT NULL, FOREIGN KEY
sqlite3.InternalErrorDatabaseErrorError interno del módulo sqlite3
sqlite3.ProgrammingErrorDatabaseErrorUso incorrecto del API
sqlite3.NotSupportedErrorDatabaseErrorFunción no soportada

Consejos de rendimiento

Activar el modo WAL

El modo Write-Ahead Logging (WAL) permite lecturas concurrentes mientras se escribe y mejora el rendimiento de escritura:

import sqlite3
 
conn = sqlite3.connect("myapp.db")
conn.execute("PRAGMA journal_mode=WAL")
# Returns 'wal' if successful

El modo WAL es especialmente útil cuando tienes un lector (p. ej., un servidor web) y un escritor (p. ej., un job en segundo plano) accediendo a la misma base de datos.

Usar executemany para inserts masivos

executemany es significativamente más rápido que llamar execute en un bucle porque reduce los “round trips” entre Python y SQLite:

import sqlite3
import time
 
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE numbers (value INTEGER)")
 
data = [(i,) for i in range(100_000)]
 
# Slow: individual inserts
start = time.perf_counter()
for d in data:
    conn.execute("INSERT INTO numbers VALUES (?)", d)
conn.commit()
slow_time = time.perf_counter() - start
 
conn.execute("DELETE FROM numbers")
 
# Fast: executemany
start = time.perf_counter()
conn.executemany("INSERT INTO numbers VALUES (?)", data)
conn.commit()
fast_time = time.perf_counter() - start
 
print(f"Individual inserts: {slow_time:.2f}s")
print(f"executemany:        {fast_time:.2f}s")
print(f"Speedup:            {slow_time / fast_time:.1f}x")
 
conn.close()

Crear índices para columnas consultadas frecuentemente

Los índices aceleran mucho las consultas SELECT a costa de escrituras ligeramente más lentas y más espacio en disco:

import sqlite3
 
conn = sqlite3.connect("myapp.db")
 
# Single column index
conn.execute("CREATE INDEX IF NOT EXISTS idx_employees_department ON employees(department)")
 
# Composite index (for queries that filter on both columns)
conn.execute("CREATE INDEX IF NOT EXISTS idx_employees_dept_salary ON employees(department, salary)")
 
# Unique index (also enforces uniqueness)
conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_employees_email ON employees(email)")
 
# Check existing indexes
cursor = conn.execute("SELECT name, sql FROM sqlite_master WHERE type='index'")
for name, sql in cursor.fetchall():
    print(f"{name}: {sql}")
 
conn.close()

Usar EXPLAIN QUERY PLAN para comprobar rendimiento

import sqlite3
 
conn = sqlite3.connect("myapp.db")
 
# Check how SQLite executes a query
result = conn.execute("""
    EXPLAIN QUERY PLAN
    SELECT * FROM employees WHERE department = 'Engineering' AND salary > 90000
""").fetchall()
 
for row in result:
    print(row)
# Output shows whether an index is used or a full table scan occurs
 
conn.close()

PRAGMAs adicionales para rendimiento

import sqlite3
 
conn = sqlite3.connect("myapp.db")
 
# Increase cache size (default is 2000 pages = ~8MB)
conn.execute("PRAGMA cache_size = -64000")  # 64MB (negative = KB)
 
# Reduce disk syncs for speed (less durable but much faster)
conn.execute("PRAGMA synchronous = NORMAL")  # Options: OFF, NORMAL, FULL (default)
 
# Store temp tables in memory
conn.execute("PRAGMA temp_store = MEMORY")
 
# Check database integrity
result = conn.execute("PRAGMA integrity_check").fetchone()
print(f"Integrity: {result[0]}")  # Should print 'ok'
 
conn.close()

Ejemplo del mundo real: Task Manager CLI

Aquí tienes una aplicación completa de gestor de tareas que demuestra todos los conceptos cubiertos en esta guía. Usa SQLite para almacenamiento persistente y soporta prioridades, etiquetas y fechas de vencimiento.

import sqlite3
import sys
from datetime import datetime, date
from contextlib import contextmanager
 
DB_PATH = "tasks.db"
 
@contextmanager
def get_db():
    """Get a database connection with Row factory and foreign keys enabled."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    conn.execute("PRAGMA journal_mode = WAL")
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()
 
def init_db():
    """Create tables if they don't exist."""
    with get_db() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS tasks (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                description TEXT DEFAULT '',
                priority INTEGER DEFAULT 2 CHECK(priority BETWEEN 1 AND 4),
                status TEXT DEFAULT 'todo' CHECK(status IN ('todo', 'in_progress', 'done')),
                due_date TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                completed_at TEXT
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS tags (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT UNIQUE NOT NULL
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS task_tags (
                task_id INTEGER NOT NULL,
                tag_id INTEGER NOT NULL,
                PRIMARY KEY (task_id, tag_id),
                FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
                FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
            )
        """)
        conn.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date)")
 
def add_task(title, description="", priority=2, due_date=None, tags=None):
    """Add a new task with optional tags."""
    with get_db() as conn:
        cursor = conn.execute(
            "INSERT INTO tasks (title, description, priority, due_date) VALUES (?, ?, ?, ?)",
            (title, description, priority, due_date)
        )
        task_id = cursor.lastrowid
 
        if tags:
            for tag_name in tags:
                conn.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag_name,))
                tag_id = conn.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)).fetchone()["id"]
                conn.execute("INSERT INTO task_tags (task_id, tag_id) VALUES (?, ?)", (task_id, tag_id))
 
        print(f"Task #{task_id} created: {title}")
        return task_id
 
def list_tasks(status=None, priority=None, tag=None):
    """List tasks with optional filters."""
    with get_db() as conn:
        conditions = []
        params = []
 
        if status:
            conditions.append("t.status = ?")
            params.append(status)
        if priority:
            conditions.append("t.priority = ?")
            params.append(priority)
        if tag:
            conditions.append("t.id IN (SELECT task_id FROM task_tags tt JOIN tags tg ON tt.tag_id = tg.id WHERE tg.name = ?)")
            params.append(tag)
 
        query = """
            SELECT t.id, t.title, t.priority, t.status, t.due_date,
                   GROUP_CONCAT(tg.name, ', ') AS tags
            FROM tasks t
            LEFT JOIN task_tags tt ON t.id = tt.task_id
            LEFT JOIN tags tg ON tt.tag_id = tg.id
        """
        if conditions:
            query += " WHERE " + " AND ".join(conditions)
        query += " GROUP BY t.id ORDER BY t.priority ASC, t.due_date ASC"
 
        rows = conn.execute(query, params).fetchall()
 
        priority_labels = {1: "URGENT", 2: "HIGH", 3: "MEDIUM", 4: "LOW"}
        print(f"\n{'ID':<5} {'Priority':<10} {'Status':<13} {'Due':<12} {'Title':<30} {'Tags'}")
        print("-" * 85)
        for row in rows:
            due = row["due_date"] or "—"
            tags_str = row["tags"] or "—"
            p_label = priority_labels.get(row["priority"], "?")
            print(f"{row['id']:<5} {p_label:<10} {row['status']:<13} {due:<12} {row['title']:<30} {tags_str}")
 
        print(f"\nTotal: {len(rows)} tasks")
 
def complete_task(task_id):
    """Mark a task as done."""
    with get_db() as conn:
        result = conn.execute(
            "UPDATE tasks SET status = 'done', completed_at = ? WHERE id = ?",
            (datetime.now().isoformat(), task_id)
        )
        if result.rowcount:
            print(f"Task #{task_id} marked as done.")
        else:
            print(f"Task #{task_id} not found.")
 
def stats():
    """Show task statistics."""
    with get_db() as conn:
        row = conn.execute("""
            SELECT
                COUNT(*) AS total,
                SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END) AS todo,
                SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress,
                SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS done,
                SUM(CASE WHEN due_date < date('now') AND status != 'done' THEN 1 ELSE 0 END) AS overdue
            FROM tasks
        """).fetchone()
 
        print(f"\nTask Statistics:")
        print(f"  Total:       {row['total']}")
        print(f"  To Do:       {row['todo']}")
        print(f"  In Progress: {row['in_progress']}")
        print(f"  Done:        {row['done']}")
        print(f"  Overdue:     {row['overdue']}")
 
# Initialize and demo
if __name__ == "__main__":
    init_db()
 
    # Add sample tasks
    add_task("Design database schema", priority=1, due_date="2026-02-20", tags=["backend", "database"])
    add_task("Write API endpoints", priority=2, due_date="2026-02-25", tags=["backend", "api"])
    add_task("Create unit tests", priority=3, tags=["testing"])
    add_task("Update documentation", priority=4, due_date="2026-03-01", tags=["docs"])
 
    # List and stats
    list_tasks()
    stats()
    complete_task(1)
    list_tasks(status="done")

SQLite vs PostgreSQL vs MySQL vs MongoDB

CaracterísticaSQLitePostgreSQLMySQLMongoDB
TipoEmbebidoCliente-servidorCliente-servidorBase documental
InstalaciónCero configInstalar + configurarInstalar + configurarInstalar + configurar
AlmacenamientoArchivo únicoDirectorio del servidorDirectorio del servidorDirectorio del servidor
Tamaño máx. DB281 TBIlimitado256 TBIlimitado
Escrituras concurrentesLimitadas (bloqueo de archivo)Excelente (MVCC)Bueno (bloqueos por fila)Bueno (bloqueos por documento)
Soporte SQLLa mayor parte de SQL92SQL completo + extensionesSQL completoMQL (no SQL)
Tipos de datos5 clases40+ tipos30+ tiposTipos BSON
Búsqueda full-textExtensión FTS5tsvector integradoFULLTEXT integradoÍndice de texto integrado
Soporte JSONExtensión JSON1JSONB nativoJSON nativoNativo (es JSON)
ReplicaciónNo integradaStreaming + lógicaPrimario-réplicaReplica sets
Mejor paraEmbebido, local, prototiposWeb apps, analíticaWeb apps, CMSEsquema flexible, documentos
Librería Pythonsqlite3 (incluida)psycopg2mysql-connectorpymongo
Hosting requeridoNo
Coste para empezarGratis, cero overheadGratis, pero requiere servidorGratis, pero requiere servidorGratis, pero requiere servidor

La decisión es sencilla: usa SQLite cuando tu aplicación sea el único proceso que escribe en la base de datos. Usa PostgreSQL o MySQL cuando necesites escrituras concurrentes multiusuario, replicación o acceso por red. Usa MongoDB cuando tus datos sean orientados a documentos y no encajen bien en un esquema relacional.

Preguntas frecuentes

¿SQLite es adecuado para producción?

Sí. SQLite se usa en producción en Android, iOS, los principales navegadores web, software de vuelo de Airbus y muchísimas aplicaciones de escritorio. Maneja hasta 281 terabytes y millones de filas. No es adecuado para aplicaciones web con alta concurrencia y muchas escrituras con muchos usuarios simultáneos, pero para la mayoría de escenarios funciona muy bien.

¿Cómo compruebo qué versión de SQLite está usando Python?

Usa sqlite3.sqlite_version para comprobar la versión de la librería SQLite y sqlite3.version para la versión del módulo Python. Por ejemplo: import sqlite3; print(sqlite3.sqlite_version) podría mostrar 3.45.1.

¿Pueden varios procesos leer la misma base de datos SQLite?

Sí. Varios procesos pueden leer la misma base de datos SQLite de forma simultánea. Las escrituras adquieren un bloqueo exclusivo sobre el archivo. Con el modo WAL activado, los lectores no bloquean a los escritores y los escritores no bloquean a los lectores. Aun así, solo un escritor puede operar a la vez.

¿Cómo hago una copia de seguridad de una base de datos SQLite de forma segura?

Usa la API de backup de sqlite3: source.backup(dest). También puedes copiar el archivo de base de datos de forma segura cuando no haya escrituras en curso. Nunca copies el archivo mientras haya una transacción de escritura activa, ya que eso puede corromper el backup.

import sqlite3
 
source = sqlite3.connect("myapp.db")
backup = sqlite3.connect("myapp_backup.db")
source.backup(backup)
backup.close()
source.close()

¿Cómo almaceno fechas y horas en SQLite?

SQLite no tiene un tipo nativo DATE o DATETIME. Almacena fechas como TEXT en formato ISO 8601 ('2026-02-18' o '2026-02-18T14:30:00'). Las funciones de fecha integradas de SQLite (date(), time(), datetime(), strftime()) funcionan con ese formato. Alternativamente, almacena timestamps Unix como valores INTEGER.

¿SQLite soporta cifrado?

El SQLite open-source estándar no incluye cifrado. SQLite Encryption Extension (SEE) es un producto comercial de pago. Alternativas gratis incluyen SQLCipher (open source) y pysqlcipher3 para integración con Python.

¿Cómo evito SQL injection en Python sqlite3?

Usa siempre consultas parametrizadas con placeholders ? o :name. Nunca construyas strings SQL usando f-strings, .format() o formateo % con input de usuario. El módulo sqlite3 gestiona el escape automáticamente cuando pasas los parámetros por separado.

Conclusión

El módulo sqlite3 de Python te ofrece una base de datos relacional completa sin configuración. Para aplicaciones locales, prototipos, scripts de análisis de datos y pruebas, SQLite es difícil de superar. Los patrones clave para recordar: usa siempre consultas parametrizadas para prevenir SQL injection, usa context managers para un manejo limpio de conexiones, activa el modo WAL para mejor concurrencia y usa executemany para operaciones masivas.

Para flujos de análisis de datos, la combinación de SQLite y pandas es muy potente: carga datos con to_sql(), consúltalos con read_sql() y visualiza resultados con PyGWalker (opens in a new tab). Cuando tu proyecto supere los límites de concurrencia de SQLite, migrar a PostgreSQL suele requerir cambios mínimos de código, ya que la interfaz DB-API 2.0 es consistente entre drivers de bases de datos.

📚