El módulo 5 terminó con la confesión más dolorosa de Ana: cada noche, al apagar el ordenador de Papyrus, el catálogo entero — objetos Libro con sus validaciones, los socios, las líneas de venta — se evapora. La RAM es rápida pero olvidadiza: cuando el programa termina, sus variables mueren con él. La solución es el disco: escribir los datos en archivos que sobreviven a los reinicios y leerlos al arrancar. En esta lección aprenderás open() y sus modos, la forma correcta de trabajar con archivos (with), las distintas maneras de leer y escribir texto, y por qué encoding="utf-8" debe acompañarte siempre. De paso saldaremos una deuda que arrastra el curso desde el módulo 1: ficha_libro.py por fin guardará las altas en un archivo.
Contenido
- Persistencia: por qué la memoria no basta
open(): la puerta al disco- Los modos de apertura:
r,w,a,x with: la forma correcta de abrir archivos- Escribir:
write()ywritelines() - Leer:
read(),readline(),readlines()e iterar línea a línea encoding="utf-8": siempre y explícito- El salto de línea:
\nal escribir,strip()al leer - Promesa saldada:
ficha_libro.pyescribe enregistro_altas.txt - Errores comunes y consejos
- Ejercicios con soluciones
Persistencia: por qué la memoria no basta
Todo lo que has creado hasta ahora vive en la memoria RAM del proceso de Python: existe mientras el programa se ejecuta y desaparece al terminar. A esa propiedad de los datos de sobrevivir entre ejecuciones se la llama persistencia, y la forma más universal de conseguirla es el sistema de archivos: texto plano hoy, CSV en 06-02, JSON en 06-03.
flowchart LR
A["RAM<br>catalogo: dict[str, Libro]<br>rápida, volátil"] -- "escribir (esta lección)" --> B["Disco<br>catalogo.txt, registro_altas.txt<br>lento, persistente"]
B -- "leer (esta lección)" --> A
El plan del módulo es exactamente ese ciclo: guardar al cerrar, cargar al abrir. Papyrus dejará de amanecer amnésica.
open(): la puerta al disco
La función integrada open() conecta tu programa con un archivo del disco y devuelve un objeto archivo con métodos para leer y escribir:
archivo = open("saludo.txt", "w", encoding="utf-8") # abre (creándolo) para escribir
archivo.write("Bienvenidos a Papyrus\n") # escribe una cadena
archivo.close() # ¡imprescindible! libera el archivoLos tres argumentos que usarás siempre:
- Ruta:
"saludo.txt"es una ruta relativa — se resuelve desde la carpeta en que ejecutas el programa. Las rutas a fondo (absolutas,pathlib) llegan en 06-04; de momento trabaja con archivos junto a tu script. - Modo:
"w","r","a"... — la siguiente sección. encoding: cómo se traducen los caracteres a bytes. Siempre"utf-8", y en su sección verás por qué no es negociable.
close() es crítico: hasta que no cierras, lo escrito puede quedarse en un búfer sin llegar al disco, y el archivo queda bloqueado para otros programas. Y ahí está el problema: si entre open() y close() algo falla, el close() no llega a ejecutarse. Por eso casi nunca escribirás open()/close() a mano — enseguida llega with.
Los modos de apertura: r, w, a, x
El segundo argumento de open() decide qué puedes hacer con el archivo y qué pasa si existe o no:
| Modo | Significado | Si el archivo existe | Si NO existe | Uso típico en Papyrus |
|---|---|---|---|---|
"r" |
read — leer (es el modo por defecto) | Lo abre para leer | Error (FileNotFoundError) |
Cargar el catálogo al arrancar |
"w" |
write — escribir | Lo vacía por completo y escribe desde cero | Lo crea | Volcar el catálogo entero al cerrar |
"a" |
append — añadir | Escribe al final, conservando lo anterior | Lo crea | Registro de altas: cada alta, una línea nueva |
"x" |
exclusive — crear en exclusiva | Error (FileExistsError) |
Lo crea | Evitar machacar un archivo por accidente |
Dos advertencias antes de continuar:
"w"es destructivo: en cuanto ejecutasopen("catalogo.txt", "w", ...), el contenido anterior ya se ha perdido — aunque luego no escribas nada. Elegir"w"donde tocaba"a"es el error más caro de esta lección.- Abrir con
"r"un archivo que no existe lanzaFileNotFoundError. El manejo elegante de errores (try/except) es el tema central del módulo 7; hasta entonces usaremos un paliativo explícito — comprobar antes conPath.exists():
from pathlib import Path # pathlib protagonizará 06-04; hoy solo le pedimos exists()
if Path("catalogo.txt").exists():
... # seguro leerlo
else:
print("Aún no hay catálogo guardado; empezamos de cero.")Que quede dicho con claridad: esta comprobación previa es un apaño temporal y tiene grietas (el archivo podría desaparecer entre la comprobación y la lectura). La herramienta correcta son las excepciones, y las aprenderás en el módulo 7.
Existen también los modos binarios ("rb", "wb") para bytes en crudo — imágenes, o el pickle que mencionaremos en 06-03 —, pero en este módulo trabajamos con texto.
with: la forma correcta de abrir archivos
La versión profesional del primer ejemplo no lleva close():
with open("saludo.txt", "w", encoding="utf-8") as archivo:
archivo.write("Bienvenidos a Papyrus\n")
# al salir del bloque, el archivo YA está cerrado — pase lo que pase dentrowith garantiza el cierre siempre: si el bloque termina bien, cierra; si dentro salta un error a medio camino, cierra igualmente antes de propagar el error. Es el equivalente a un close() imposible de olvidar. La variable tras as (archivo) es el mismo objeto archivo de antes, solo que con el ciclo de vida gestionado.
with funciona con archivos porque estos implementan el protocolo de los context managers — un mecanismo general de Python que también sirve para conexiones, bloqueos y recursos propios. Ese protocolo completo (y cómo escribir los tuyos) se estudia en el módulo 8; en este módulo te basta la regla práctica: todo open() va dentro de un with. Así lo haremos en el resto del curso sin excepción.
Escribir: write() y writelines()
write(cadena) escribe exactamente la cadena que le pasas — ni más ni menos. Dos consecuencias inmediatas:
- No añade salto de línea: si quieres líneas, escribe tú el
"\n". - Solo acepta cadenas: los números hay que convertirlos (las f-strings del módulo 1 son perfectas para esto).
libros = [("La Odisea", 12.50, 4), ("Hamlet", 9.95, 6), ("El Quijote", 15.90, 8), ("Fausto", 21.00, 0)]
with open("catalogo.txt", "w", encoding="utf-8") as f:
f.write("Catálogo de Papyrus\n") # el \n lo pones tú
for titulo, precio, stock in libros:
f.write(f"{titulo} | {precio:.2f} | {stock}\n") # números → texto vía f-stringContenido resultante de catalogo.txt:
Catálogo de Papyrus La Odisea | 12.50 | 4 Hamlet | 9.95 | 6 El Quijote | 15.90 | 8 Fausto | 21.00 | 0
writelines(lista) escribe todas las cadenas de una lista... y, pese a su nombre, tampoco añade saltos de línea. Es útil cuando ya tienes las líneas preparadas:
lineas = [f"{t} | {p:.2f} | {s}\n" for t, p, s in libros] # comprensión (02-04), \n incluido
with open("catalogo.txt", "w", encoding="utf-8") as f:
f.write("Catálogo de Papyrus\n")
f.writelines(lineas)Fíjate en el separador |: lo hemos inventado nosotros, y funciona… hasta que un título contenga una barra vertical. Inventar formatos con separadores caseros es frágil — exactamente el problema que el formato CSV y su módulo estándar resuelven en 06-02.
Leer: read(), readline(), readlines() e iterar línea a línea
Para leer tienes cuatro caminos; elegir bien depende del tamaño del archivo:
| Método | Devuelve | Memoria | Cuándo usarlo |
|---|---|---|---|
f.read() |
TODO el contenido en una sola cadena | Todo el archivo a la vez | Archivos pequeños que procesas como bloque |
f.readline() |
La siguiente línea (con su \n) |
Una línea | Leer una cabecera y parar |
f.readlines() |
Lista con todas las líneas (con \n) |
Todo el archivo a la vez | Necesitas la lista completa (contar, indexar) |
for linea in f: |
Una línea por vuelta | Una línea cada vez | El modo por defecto: procesar línea a línea |
with open("catalogo.txt", "r", encoding="utf-8") as f:
contenido = f.read() # una única cadena con \n incluidos
print(len(contenido))Para un catalogo.txt de cinco líneas cualquier método vale. Pero imagina el histórico de ventas de diez años de Papyrus: read() y readlines() lo cargarían entero en RAM. Iterar sobre el archivo, en cambio, lee una línea, la procesa, la olvida y pasa a la siguiente — memoria constante, da igual que el archivo tenga cinco líneas o cinco millones:
with open("catalogo.txt", "r", encoding="utf-8") as f:
next(f) # salta la cabecera "Catálogo de Papyrus"
for linea in f: # el objeto archivo es iterable (02-02)
titulo, precio, stock = linea.strip().split(" | ") # 04-05: strip + split
print(f"{titulo}: {float(precio):.2f} € — stock {int(stock)}")Observa las conversiones float(precio) e int(stock): un archivo de texto solo contiene texto. El 12.50 que escribiste como número vuelve como la cadena "12.50", y recuperar los tipos es responsabilidad tuya. Este "peaje de conversión" reaparecerá en CSV (06-02) y desaparecerá casi del todo con JSON (06-03).
encoding="utf-8": siempre y explícito
Un archivo de texto es, en el fondo, una secuencia de bytes, y el encoding es el diccionario que traduce caracteres ↔ bytes. Con las letras ASCII (a–z) todos los diccionarios coinciden; el problema son las tildes, la ñ, €, ·… Si no pasas encoding, Python usa el de la plataforma: UTF-8 en macOS/Linux, pero históricamente cp1252 en Windows. El mismo programa que en tu portátil escribe Antología puede leer AntologÃa (o lanzar UnicodeDecodeError) en el ordenador de la tienda.
# MAL: encoding implícito — depende de la máquina que ejecute
with open("catalogo.txt", "w") as f:
f.write("Antología de García Lorca — 14 €\n")
# BIEN: explícito — el archivo es idéntico en Windows, macOS y Linux
with open("catalogo.txt", "w", encoding="utf-8") as f:
f.write("Antología de García Lorca — 14 €\n")Regla del curso, sin excepciones: todo open() de texto lleva encoding="utf-8", al escribir y al leer (con el mismo diccionario en ambas direcciones, claro). UTF-8 es el estándar de facto de la web y de casi todo formato moderno. Una librería con ñ en la mitad de sus fichas no puede permitirse otra cosa.
El salto de línea: \n al escribir, strip() al leer
El carácter \n que separas las líneas al escribir viaja dentro de cada línea al leer: readline(), readlines() y la iteración lo conservan al final de la cadena. Si lo olvidas, aparecen comparaciones que fallan y prints con líneas en blanco fantasma:
with open("catalogo.txt", "r", encoding="utf-8") as f:
primera = f.readline()
print(repr(primera)) # 'Catálogo de Papyrus\n' ← ahí está
print(primera == "Catálogo de Papyrus") # False (por culpa del \n)
print(primera.strip() == "Catálogo de Papyrus") # TrueEl antídoto es viejo conocido: strip() (04-05) al procesar cada línea leída — elimina el \n final y de regalo los espacios accidentales. El patrón canónico de lectura queda así: for linea in f: + linea.strip() como primer paso. (Python además normaliza automáticamente los saltos de Windows \r\n a \n en modo texto, así que strip() te cubre también ese caso.)
Promesa saldada: ficha_libro.py escribe en registro_altas.txt
En el módulo 1, ficha_libro.py pedía los datos de un libro con input() y mostraba la ficha con print() — y prometimos que algún día esa ficha no moriría al cerrar la ventana. Ese día es hoy. La versión definitiva da de alta el libro y deja constancia en disco, usando el modo "a" para que cada ejecución añada una línea sin borrar las anteriores:
"""ficha_libro.py — alta de libros con registro persistente (promesa del módulo 1)."""
from dataclasses import dataclass
from datetime import date # biblioteca estándar, 03-05
NOMBRE_TIENDA = "Papyrus"
REGISTRO = "registro_altas.txt"
@dataclass
class Libro: # la dataclass canónica de 05-06 (versión mínima)
titulo: str
precio: float
stock: int = 0
def __post_init__(self):
self.titulo = self.titulo.strip()
if not self.titulo:
raise ValueError("El título no puede estar vacío")
if self.precio < 0:
raise ValueError(f"Precio negativo: {self.precio}")
def registrar_alta(libro):
"""Añade una línea al registro de altas. Modo 'a': nunca borra lo anterior."""
with open(REGISTRO, "a", encoding="utf-8") as f:
f.write(f"{date.today()} | ALTA | {libro.titulo} | {libro.precio:.2f} | {libro.stock}\n")
if __name__ == "__main__": # 03-04: ejecutable e importable
titulo = input("Título: ")
precio = float(input("Precio (€): "))
stock = int(input("Stock: "))
libro = Libro(titulo, precio, stock)
registrar_alta(libro)
print(f"{libro.titulo} dado de alta en {NOMBRE_TIENDA} y registrado en {REGISTRO}.")Tras dar de alta dos libros en días distintos, registro_altas.txt acumula la historia:
Cada pieza viene de un módulo anterior: input()/f-strings (M1), la dataclass validando al nacer (05-06), date.today() (03-05), y lo nuevo — with + "a" + encoding="utf-8" — pone la persistencia. Ana ya tiene algo que el apagón nocturno no puede borrar.
Errores Comunes y Consejos
- Abrir con
"w"cuando querías"a": el archivo se vacía en el instante delopen(). Si es un registro histórico (altas, ventas), es"a";"w"es para volcados completos que se regeneran enteros. - Olvidar
encoding="utf-8": funciona en tu máquina y revienta (o corrompe tildes) en otra. Explícito siempre, en lectura y escritura. - Esperar que
write()owritelines()añadan\n: no lo hacen. Tú pones los saltos; el nombrewritelinesengaña. - Olvidar
strip()al leer: el\ninvisible hace fallar comparaciones ysplit(). Primer reflejo con cada línea leída:linea.strip(). - Leer con
read()/readlines()archivos potencialmente grandes: cargan todo en RAM. El modo por defecto es iterar:for linea in f:. - Escribir fuera del
with: tras el bloque, el archivo está cerrado;f.write(...)lanzaValueError: I/O operation on closed file. Todo el trabajo conf, dentro del bloque. - Olvidar convertir tipos al leer: del archivo todo vuelve como
str."12.50" + 1es unTypeError; convierte confloat()/int()nada más hacersplit(). - Consejo: los archivos de texto se inspeccionan gratis — ábrelos con cualquier editor para verificar qué escribió tu programa. Esa transparencia es una gran ventaja frente a formatos binarios, aprovéchala mientras depuras.
Ejercicios
Ejercicio 1: inventario nocturno
Escribe guardar_inventario(catalogo, ruta) que reciba el catálogo canónico dict[str, Libro] (05-06) y escriba en ruta una línea por libro con el formato titulo | precio | stock (precio con 2 decimales), en modo "w" — es el volcado completo de cierre de caja, se regenera entero cada noche. Pruébalo con los cuatro libros canónicos y abre el archivo con tu editor para comprobarlo.
Ejercicio 2: recargar el catálogo al abrir la tienda
Escribe cargar_inventario(ruta) que haga el viaje inverso: lea el archivo del ejercicio 1 línea a línea (iterando, no con read()), reconstruya cada Libro con los tipos correctos y devuelva el dict[str, Libro] con claves normalizadas (normalizar_titulo, 04-05). Si el archivo no existe, devuelve {} usando el paliativo Path.exists() — y deja un comentario recordando que el módulo 7 traerá la solución de verdad. Comprueba que cargar_inventario("inventario.txt")["hamlet"].precio_final(socio=True) devuelve 9.83.
Ejercicio 3: diario de apertura y cierre
Escribe anotar(evento) que añada a diario.txt (modo "a") una línea fecha | evento, con la fecha de date.today(). Llama a anotar("Apertura de Papyrus") y anotar("Cierre de caja"), ejecuta el script dos veces y comprueba que el diario conserva las cuatro líneas. Después escribe contar_eventos(ruta) que devuelva un Counter (04-06) de eventos leyendo el diario línea a línea.
Soluciones
Solución 1:
def guardar_inventario(catalogo, ruta):
with open(ruta, "w", encoding="utf-8") as f: # "w": volcado completo
for libro in catalogo.values():
f.write(f"{libro.titulo} | {libro.precio:.2f} | {libro.stock}\n")
guardar_inventario(catalogo, "inventario.txt")Iteramos sobre values() (04-03) porque la clave normalizada es reconstruible; el título "bonito" vive en el objeto.
Solución 2:
from pathlib import Path
def cargar_inventario(ruta):
if not Path(ruta).exists(): # paliativo hasta el try/except del módulo 7
return {}
catalogo = {}
with open(ruta, "r", encoding="utf-8") as f:
for linea in f: # línea a línea: memoria constante
titulo, precio, stock = linea.strip().split(" | ")
libro = Libro(titulo, float(precio), int(stock)) # str → tipos reales
catalogo[normalizar_titulo(libro.titulo)] = libro
return catalogo
catalogo = cargar_inventario("inventario.txt")
print(catalogo["hamlet"].precio_final(socio=True)) # 9.83 — la cifra canónicaEl round-trip completo: los objetos que guardaste anoche vuelven a la vida esta mañana, validados por __post_init__ al reconstruirse.
Solución 3:
from datetime import date
from collections import Counter
def anotar(evento):
with open("diario.txt", "a", encoding="utf-8") as f:
f.write(f"{date.today()} | {evento}\n")
def contar_eventos(ruta):
eventos = Counter()
with open(ruta, "r", encoding="utf-8") as f:
for linea in f:
_, evento = linea.strip().split(" | ")
eventos[evento] += 1
return eventos
anotar("Apertura de Papyrus")
anotar("Cierre de caja")
print(contar_eventos("diario.txt"))
# tras dos ejecuciones: Counter({'Apertura de Papyrus': 2, 'Cierre de caja': 2})El modo "a" acumula entre ejecuciones — eso es exactamente la persistencia que Papyrus necesitaba.
Conclusión
Papyrus ya tiene memoria a largo plazo: open() con el modo adecuado (r para leer, w para volcar, a para acumular, x para proteger), siempre dentro de un with que garantiza el cierre, siempre con encoding="utf-8" para que las tildes y las ñ sobrevivan al viaje, escribiendo \n a conciencia y limpiando con strip() al volver. La promesa del módulo 1 está saldada: ficha_libro.py deja huella en registro_altas.txt y el inventario resucita cada mañana. Pero nuestro formato titulo | precio | stock es un dialecto casero: lo inventamos nosotros, nadie más lo entiende, y se rompe el día que un título contenga una barra vertical. Existe un formato de texto tabular que Excel, Google Sheets y medio mundo hablan de nacimiento, con un módulo de la biblioteca estándar que resuelve por ti separadores, comillas y casos retorcidos: CSV. En la próxima lección, el catálogo y las ventas de Papyrus se mudan a él.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
