lunes, 19 de enero de 2026

Aprendiendo Python de 0 a experto - Archivos y Persistencia de Datos

Archivos y Persistencia de Datos en Python
Python // Chapter 08

Archivos y Persistencia de Datos

Domina el arte de leer, escribir y persistir datos en Python

Las aplicaciones del mundo real no solo ejecutan código en memoria y terminan sin dejar rastro. Interactúan con redes, discos y bases de datos. Intercambian información con otras aplicaciones usando formatos adecuados para cada situación.

En este capítulo exploraremos el manejo de archivos y directorios, compresión, formatos de intercambio de datos como JSON, y la persistencia de datos usando pickle, shelve y SQLAlchemy.

MÓDULO_01

Trabajando con Archivos

Python ofrece herramientas intuitivas para trabajar con archivos. La función open() es tu puerta de entrada al sistema de archivos, permitiendo leer y escribir datos de manera simple y segura.

Abriendo Archivos

Modos de Apertura
"r" - Read
"w" - Write
"a" - Append
"x" - Exclusive
open_basic.py
# Forma básica (NO recomendada)
fh = open("data.txt", "rt")  # r: read, t: text
for line in fh.readlines():
    print(line.strip())
fh.close()  # ¡No olvides cerrar!

# Forma segura con try/finally
fh = open("data.txt")
try:
    for line in fh:
        print(line.strip())
finally:
    fh.close()  # Siempre se ejecuta

Context Managers: La Forma Pythonica

Los context managers garantizan que el archivo se cierre automáticamente, incluso si ocurre un error:

open_with.py
# ✓ RECOMENDADO: Usando context manager
with open("fear.txt") as fh:
    for line in fh:
        print(line.strip())
# El archivo se cierra automáticamente aquí

# Lectura y escritura
with open("source.txt") as f:
    lines = [line.rstrip() for line in f]

with open("copy.txt", "w") as fw:
    fw.write("\n".join(lines))
El statement with llama automáticamente a fh.close() cuando sale del bloque, incluso si ocurre una excepción. Es la forma más segura y limpia de trabajar con archivos.

Modo Binario

Para archivos que no son texto puro (imágenes, audio, formatos propietarios), usa el modo binario:

read_write_bin.py
# Escribir en modo binario
with open("example.bin", "wb") as fw:
    fw.write(b"Binary data here...")

# Leer en modo binario
with open("example.bin", "rb") as f:
    print(f.read())  # b'Binary data here...'

Protección contra Sobreescritura

write_not_exists.py
# Modo "x": solo crea si NO existe
with open("new_file.txt", "x") as fw:
    fw.write("Primera línea")  # ✓ Funciona

with open("new_file.txt", "x") as fw:
    fw.write("Segunda línea")  # ✗ FileExistsError
MÓDULO_02

Pathlib: Rutas Modernas

El módulo pathlib proporciona clases que representan rutas del sistema de archivos con semánticas apropiadas para diferentes sistemas operativos.

PORTABILIDAD
Maneja separadores de ruta automáticamente entre Windows/Unix
OPERADOR /
Concatena rutas de forma intuitiva con el operador división
API RICA
Métodos útiles: exists(), is_file(), is_dir(), glob()
paths.py
from pathlib import Path

p = Path("data.txt")

print(p.absolute())      # /home/user/project/data.txt
print(p.name)            # data.txt
print(p.parent)          # . (directorio actual)
print(p.suffix)          # .txt
print(p.stem)            # data
print(p.parts)           # ('data.txt',)

# Concatenación elegante con /
readme = p.parent / ".." / "docs" / "README.md"
print(readme.resolve())  # Ruta absoluta resuelta

Verificar Existencia

existence.py
from pathlib import Path

p = Path("config.json")

print(p.exists())       # True/False
print(p.is_file())      # True si es archivo
print(p.is_dir())       # True si es directorio

# Verificar directorio
folder = Path("/Users/dev/projects")
print(folder.is_dir())  # True

Manipulación de Archivos y Directorios

manipulation.py
import shutil
from pathlib import Path

base = Path("project")

# Crear estructura de directorios
(base / "src" / "utils").mkdir(parents=True)
(base / "tests").mkdir()

# Crear archivos
for name in ("main.py", "config.py"):
    with open(base / "src" / name, "w") as f:
        f.write(f"# {name}\n")

# Renombrar
old = base / "src" / "config.py"
old.rename(old.parent / "settings.py")

# Mover directorio completo
shutil.move(base / "src", base / "source")

Archivos y Directorios Temporales

tmp.py
from tempfile import NamedTemporaryFile, TemporaryDirectory

with TemporaryDirectory(dir=".") as td:
    print("Temp dir:", td)
    
    with NamedTemporaryFile(dir=td) as t:
        print("Temp file:", t.name)
        t.write(b"Temporary data")

# Ambos se eliminan automáticamente al salir

Listar Contenido de Directorios

listing.py
from pathlib import Path

p = Path(".")

# Listar todo en el directorio actual
for entry in p.glob("*"):
    tipo = "File:" if entry.is_file() else "Dir:"
    print(tipo, entry)

# Buscar recursivamente archivos Python
for py_file in p.glob("**/*.py"):
    print(py_file)

# Usando walk() para escanear árbol completo
for root, dirs, files in p.walk():
    print(f"Root: {root}")
    for f in files:
        print(f"  - {f}")
MÓDULO_03

Compresión de Archivos

Python permite crear y extraer archivos comprimidos en varios formatos. El formato ZIP es uno de los más comunes:

compression.py
from zipfile import ZipFile

# Crear archivo ZIP
with ZipFile("backup.zip", "w") as zp:
    zp.write("config.json")
    zp.write("data/users.csv")
    zp.write("data/products.csv")

# Extraer archivos específicos
with ZipFile("backup.zip") as zp:
    zp.extract("config.json", "restored/")
    zp.extractall("full_restore/")  # Todo
Python también soporta formatos tar.gz, gzip y bz2 mediante los módulos tarfile, gzip y bz2 respectivamente.
MÓDULO_04

Formato JSON

JSON (JavaScript Object Notation) es quizás el formato de intercambio de datos más usado en Python. Forma parte de la biblioteca estándar y ofrece simplicidad y compatibilidad universal.

Mapeo Python ↔ JSON
dict
object {}
list
array []
str
string
None
null

Serialización Básica

json_basic.py
import json

data = {
    "name": "Sherlock Holmes",
    "address": {
        "street": "221B Baker St",
        "city": "London",
        "country": "UK"
    },
    "cases_solved": 156
}

# Serializar a string JSON
json_str = json.dumps(data, indent=2, sort_keys=True)
print(json_str)

# Deserializar de vuelta a Python
data_back = json.loads(json_str)
assert data == data_back  # ✓ Idénticos
python json_basic.py
{
  "address": {
    "city": "London",
    "country": "UK",
    "street": "221B Baker St"
  },
  "cases_solved": 156,
  "name": "Sherlock Holmes"
}

Trabajando con Archivos

json_files.py
import json

# Guardar en archivo
with open("config.json", "w") as f:
    json.dump(data, f, indent=2)

# Cargar desde archivo
with open("config.json") as f:
    loaded = json.load(f)

Encoder Personalizado

JSON no puede serializar todos los tipos de Python. Para tipos personalizados, creamos un encoder:

json_custom.py
import json

class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, complex):
            return {
                "_meta": "complex",
                "num": [obj.real, obj.imag]
            }
        return super().default(obj)

data = {
    "value": 3 + 4j,
    "name": "complex number"
}

# Serializar con encoder personalizado
json_data = json.dumps(data, cls=ComplexEncoder)
print(json_data)

# Deserializar con object_hook
def decode_complex(obj):
    if obj.get("_meta") == "complex":
        return complex(*obj["num"])
    return obj

restored = json.loads(json_data, object_hook=decode_complex)
print(restored["value"])  # (3+4j)
Las tuplas de Python se convierten a arrays JSON (listas). Al deserializar, se pierden como tuplas y vuelven como listas. Ten cuidado con esta pérdida de información de tipo.
MÓDULO_05

I/O y Streams

Python ofrece streams en memoria que pueden ser útiles para operaciones de I/O sin tocar el disco.

StringIO: Streams en Memoria

string_io.py
import io

with io.StringIO() as stream:
    stream.write("Learning Python.\n")
    print("Become a ninja!", file=stream)
    
    contents = stream.getvalue()
    print(contents)
# Stream se cierra automáticamente
python string_io.py
Learning Python.
Become a ninja!

Peticiones HTTP con requests

http_requests.py
import requests

urls = {
    "ip": "https://httpbin.org/ip",
    "headers": "https://httpbin.org/headers",
    "json": "https://httpbin.org/json",
}

for name, url in urls.items():
    resp = requests.get(url)
    print(f"=== {name} ===")
    print(resp.json())  # Auto-decode JSON
MÓDULO_06

Persistencia con Pickle

El módulo pickle convierte objetos Python a flujos de bytes y viceversa. A diferencia de JSON, soporta casi cualquier tipo de dato Python, pero es específico del lenguaje.

Pickle vs JSON
PICKLE
Binario
Python-only
Soporta todo
vs
JSON
Texto
Universal
Tipos básicos
pickler.py
import pickle
from dataclasses import dataclass

@dataclass
class Person:
    first_name: str
    last_name: str
    id: int
    
    def greet(self):
        print(f"Hi, I'm {self.first_name} {self.last_name}")

people = [
    Person("Obi-Wan", "Kenobi", 123),
    Person("Anakin", "Skywalker", 456),
]

# Guardar (serializar)
with open("data.pickle", "wb") as f:
    pickle.dump(people, f)

# Cargar (deserializar)
with open("data.pickle", "rb") as f:
    loaded = pickle.load(f)

for person in loaded:
    person.greet()  # ¡Los métodos también funcionan!
python pickler.py
Hi, I'm Obi-Wan Kenobi
Hi, I'm Anakin Skywalker
⚠️ SEGURIDAD: Nunca hagas unpickle de datos de fuentes no confiables. Datos maliciosos pueden ejecutar código arbitrario. Considera usar firmas criptográficas para verificar la integridad.
MÓDULO_07

Shelve: Diccionario Persistente

Un "shelf" es un diccionario persistente. Los valores pueden ser cualquier objeto que pickle pueda serializar, dándote flexibilidad sin la complejidad de una base de datos.

shelf.py
import shelve

class Person:
    def __init__(self, name, id):
        self.name = name
        self.id = id

with shelve.open("myshelf.db") as db:
    # Guardar como diccionario
    db["obi1"] = Person("Obi-Wan", 123)
    db["ani"] = Person("Anakin", 456)
    db["primes"] = [2, 3, 5, 7]
    
    print(list(db.keys()))  # ['obi1', 'ani', 'primes']
    
    # Eliminar
    del db["ani"]
    
    # Verificar membresía
    print("obi1" in db)  # True
    print("ani" in db)   # False

Modo Writeback

shelf_writeback.py
# Sin writeback: hay que reasignar
with shelve.open("shelf1.db") as db:
    db["nums"] = [1, 2, 3]
    nums = db["nums"]
    nums.append(4)
    db["nums"] = nums  # Necesario

# Con writeback: modificación in-place
with shelve.open("shelf2.db", writeback=True) as db:
    db["nums"] = [1, 2, 3]
    db["nums"].append(4)  # ¡Funciona directamente!
El modo writeback=True consume más memoria y hace el cierre más lento, pero permite modificaciones in-place más intuitivas.
MÓDULO_08

SQLAlchemy: ORM Profesional

SQLAlchemy es el ORM (Object-Relational Mapping) más popular de Python. Permite interactuar con bases de datos relacionales usando objetos Python en lugar de SQL crudo.

PORTABILIDAD
Cambia de SQLite a PostgreSQL sin modificar código
PYTHONIC
Trabaja con objetos, no con strings SQL
RELACIONES
Maneja foreign keys y joins automáticamente

Definición de Modelos

alchemy_models.py
from sqlalchemy import ForeignKey, String, Integer
from sqlalchemy.orm import (
    DeclarativeBase, mapped_column, relationship
)

class Base(DeclarativeBase):
    pass

class Person(Base):
    __tablename__ = "person"
    
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)
    age = mapped_column(Integer)
    
    emails = relationship(
        "Email",
        back_populates="person",
        cascade="all, delete-orphan"
    )
    
    def __repr__(self):
        return f"{self.name}(id={self.id})"

class Email(Base):
    __tablename__ = "email"
    
    id = mapped_column(Integer, primary_key=True)
    email = mapped_column(String)
    person_id = mapped_column(ForeignKey("person.id"))
    
    person = relationship("Person", back_populates="emails")

Operaciones CRUD

alchemy.py
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from alchemy_models import Person, Email, Base

# Crear engine (en memoria para demo)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    # CREATE
    anakin = Person(name="Anakin Skywalker", age=32)
    anakin.emails.append(Email(email="ani@example.com"))
    anakin.emails.append(Email(email="vader@empire.gov"))
    
    session.add(anakin)
    session.commit()
    
    # READ
    query = select(Person).where(Person.name.like("Anakin%"))
    result = session.scalar(query)
    print(result, result.emails)
    
    # UPDATE
    result.age = 45
    session.commit()
    
    # DELETE
    session.delete(result)
    session.commit()
python alchemy.py
Anakin Skywalker(id=1) [ani@example.com, vader@empire.gov]
Al eliminar a anakin, sus emails también se eliminan automáticamente gracias a cascade="all, delete-orphan". SQLAlchemy maneja las relaciones por ti.
MÓDULO_09

Archivos de Configuración

Los archivos de configuración permiten separar settings del código, facilitando el mantenimiento y la gestión de diferentes entornos (desarrollo, producción, testing).

Formato INI

config.ini
[owner]
name = Fabrizio Romano
dob = 1975-12-29T11:50:00Z

[DEFAULT]
host = 192.168.1.1

[database]
host = 192.168.1.255
user = redis
password = secret-password
port = 6379
read_ini.py
from configparser import ConfigParser

config = ConfigParser()
config.read("config.ini")

print(config["database"]["host"])  # 192.168.1.255
print(config["database"]["port"])  # 6379
print(config["owner"]["name"])     # Fabrizio Romano

Formato TOML

TOML es más moderno y popular en proyectos Python. Soporta tipos de datos nativos como listas, números y fechas:

config.toml
title = "Config Example"

[owner]
name = "Fabrizio Romano"
dob = 1975-12-29T11:50:00Z

[database]
host = "192.168.1.255"
ports = [6379, 6380]
enabled = true

[database.primary]
port = 6379
connection_max = 5000
read_toml.py
import tomllib

with open("config.toml", "rb") as f:
    config = tomllib.load(f)

print(config["title"])                    # Config Example
print(config["database"]["ports"])        # [6379, 6380]
print(config["database"]["primary"])     # {'port': 6379, ...}
print(config["owner"]["dob"])            # datetime object!
TOML convierte automáticamente los valores a tipos Python apropiados: strings, números, listas, booleanos, y hasta objetos datetime.

Resumen del Capítulo

01 // Archivos

Usa context managers (with) para garantizar cierre automático. Pathlib para rutas modernas y portables.

02 // JSON

Formato de intercambio universal. Usa encoders personalizados para tipos no nativos.

03 // Pickle & Shelve

Serialización Python-nativa. Potente pero solo para datos confiables.

04 // SQLAlchemy

ORM profesional para bases de datos. Trabaja con objetos, no SQL crudo.

05 // I/O Streams

StringIO para operaciones en memoria. Requests para HTTP.

06 // Configuración

INI para simplicidad, TOML para tipos ricos. Separa config del código.

Basado en "Learn Python Programming" 4th Edition // Fabrizio Romano

Chapter 8: Files and Data Persistence