Saltar a contenido

1.10 · Archivos

Objetivos. Al terminar este capítulo podrás: leer y escribir ficheros de forma segura con with, elegir el modo de apertura, trabajar con JSON y manejar rutas multiplataforma con pathlib.

Evidencia de logro. Escribirás un asistente de notas que guarda y recupera datos en un fichero (y en JSON), sobreviviendo al cierre del programa.

Contexto y motivación

Si los datos desaparecen al cerrar el programa, no se pueden recuperar en la siguiente ejecución. Los ficheros son una forma sencilla de persistir información: notas, configuraciones, resultados o logs. En IA guardarás datasets, modelos y métricas; aquí aprenderás el mecanismo básico que después usarás con librerías más especializadas.

Dos ideas clave antes de empezar:

  • Cuando abras un fichero, ciérralo (o usa with, que lo hace por ti). De lo contrario, puede quedar abierto o escribirse solo una parte de los datos.
  • Para construir rutas portables, usa pathlib en lugar de concatenar cadenas con "/".

Vocabulario

Término Significado
Fichero Datos guardados en disco (texto, JSON, binario)
Modo Cómo se abre: lectura (r), escritura (w), añadir (a)
with Context manager que cierra el fichero automáticamente
Serializar Convertir a texto/bytes para guardar (JSON)
Ruta Ubicación del fichero (/carpeta/archivo.txt)

Prerrequisitos

1.9 · Cadenas y manejo básico de rutas. Aquí anticipamos FileNotFoundError para tratar la primera ejecución; su modelo completo se estudia en 1.11 · Excepciones y logging.

Dónde se escriben los ficheros de este capítulo

Todos los ejemplos trabajan en una carpeta de trabajo propia, practica/ud1_archivos/, que se crea en el primer bloque. No uses la carpeta datos/ del curso: ahí viven los ficheros fijos que usa todo el alumnado y no deben cambiar.

El apartado 1 crea la carpeta de trabajo y el fichero notas.txt, y los apartados siguientes reutilizan las variables carpeta y ruta. Si copias un apartado suelto, ejecuta antes el bloque del apartado 1.

1. Escribir y leer con with

Para escribir, abrimos en modo "w" (crea o sobrescribe):

from pathlib import Path

carpeta = Path("practica/ud1_archivos")
carpeta.mkdir(parents=True, exist_ok=True)
ruta = carpeta / "notas.txt"

with open(ruta, "w", encoding="utf-8") as f:
    f.write("hola\nmundo\n")

with open(ruta, encoding="utf-8") as f:
    contenido = f.read()

print(contenido)

Salida esperada:

hola
mundo

Este bloque crea la carpeta practica/ud1_archivos/ y dentro el fichero notas.txt; por eso la comprobación visible es el read() que imprime su contenido. La línea en blanco final de la salida no es un error: el texto escrito termina en \n y print() añade otro salto de línea.

Paso a paso:

  • Path(...) representa una ruta (lo verás a fondo en el apartado 5) y mkdir(parents=True, exist_ok=True) crea la carpeta si no existe.
  • open(ruta, "w", encoding="utf-8") abre el fichero para escribir y devuelve un objeto fichero, que llamamos f por costumbre.
  • with ... as f: abre el fichero y lo cierra automáticamente al salir del bloque indentado, aunque haya un error. Es el patrón a usar siempre.
  • f.write(texto) escribe; f.read() devuelve todo el contenido como una cadena. Fíjate en que write no añade saltos de línea: hay que escribir \n.

2. Modos de apertura

Modo Qué hace
"r" (por defecto) Lectura; error si el fichero no existe
"w" Escritura; sobrescribe o crea
"a" Añadir al final (append); crea si no existe
"r+" Lectura y escritura (no lo trunca)
with open(ruta, "a", encoding="utf-8") as f:
    f.write("otra linea\n")

with open(ruta, encoding="utf-8") as f:
    print(f.read().split())

Salida esperada:

['hola', 'mundo', 'otra', 'linea']

"a" añade al final sin borrar lo anterior. Usa encoding="utf-8" para que los acentos y caracteres no ASCII se guarden bien.

3. Leer línea a línea

Para ficheros grandes, leer línea a línea evita cargar todo en memoria:

with open(ruta, encoding="utf-8") as f:
    for linea in f:
        print(">>", linea.strip())

Salida esperada:

>> hola
>> mundo
>> otra linea

Iterar el fichero (for linea in f) es la forma eficiente: solo hay una línea en memoria a la vez. readlines() carga todas, útil para ficheros pequeños.

4. Trabajar con JSON

JSON es el formato de intercambio de datos por excelencia (APIs, configs). Se serializa con json.dump y se deserializa con json.load:

import json

datos = {"nombre": "Ana", "edad": 20, "ciudad": "Sevilla"}

with open(carpeta / "datos.json", "w", encoding="utf-8") as f:
    json.dump(datos, f, ensure_ascii=False, indent=2)

with open(carpeta / "datos.json", encoding="utf-8") as f:
    recuperado = json.load(f)

print(recuperado)

Salida esperada:

{'nombre': 'Ana', 'edad': 20, 'ciudad': 'Sevilla'}

Este bloque crea datos.json dentro de la carpeta de trabajo y lo vuelve a leer. ensure_ascii=False conserva los acentos; indent=2 lo formatea legible. JSON convierte dict a texto y vuelta; es la base de cualquier API que verás en la UD2.

JSON vs texto plano

El texto plano es fácil de leer; JSON tiene estructura (claves, listas, anidados) y lo entienden casi todos los lenguajes. Para datos con forma, JSON.

5. Rutas y ficheros con pathlib

pathlib es la forma moderna y multiplataforma de manejar rutas y ficheros. Un Path es un objeto que representa una ruta: puedes inspeccionarla, construirla y leer/escribir sobre ella. Sustituye a os.path y a las cadenas con "/" que fallan según el sistema operativo.

5.1 Crear un Path y leer sus componentes

from pathlib import Path

p = Path("docs/ud1/notas.txt")
print(p.name)       # nombre del fichero
print(p.stem)       # sin extensión
print(p.suffix)     # extensión
print(p.parent.as_posix())   # carpeta contenedora, siempre con "/"
print(p.parts)      # trozos de la ruta

Salida esperada:

notas.txt
notas
.txt
docs/ud1
('docs', 'ud1', 'notas.txt')

Path funciona también con rutas que aún no existen: puedes preguntarle por el nombre, la extensión o la carpeta sin abrir nada.

5.2 Construir rutas con /

El operador / une rutas de forma multiplataforma (sin concatenar cadenas):

base = Path("datos")
doc = base / "ventas" / "2026"

print(doc.as_posix())                    # ruta con "/" en cualquier sistema
print((doc / "enero.csv").as_posix())

Salida esperada:

datos/ventas/2026
datos/ventas/2026/enero.csv

Path("datos") / "ventas" / "2026" construye la ruta correcta tanto en Windows como en Linux/macOS. Adiós a os.path.join y a las barras escritas a mano. Al mostrarla, as_posix() la escribe siempre con /, también en Windows.

5.3 Inspeccionar: existe, es fichero, es carpeta

p = carpeta / "inspeccion.txt"
p.write_text("fichero de ejemplo\n", encoding="utf-8")

print(p.exists())     # ¿existe la ruta?
print(p.is_file())    # ¿es un fichero?
print(p.is_dir())     # ¿es una carpeta?

Salida esperada:

True
True
False

is_dir() es False porque p apunta a un fichero, aunque esté dentro de una carpeta. exists() solo pregunta si hay algo en esa ruta; is_file() e is_dir() preguntan además de qué tipo es.

5.4 Leer y escribir con pathlib

p = carpeta / "lectura.txt"
p.write_text("hola\nmundo\n", encoding="utf-8")
print(p.read_text(encoding="utf-8").split())

with p.open(encoding="utf-8") as fichero:
    for numero, linea in enumerate(fichero, start=1):
        print(numero, linea.strip())

Salida esperada:

['hola', 'mundo']
1 hola
2 mundo

write_text()/read_text() simplifican el with open(...). Para ficheros grandes, p.open() te da el mismo comportamiento eficiente de leer línea a línea; úsalo dentro de with para cerrar el fichero automáticamente.

Aquí enumerate(fichero, start=1) numera las líneas empezando en 1, igual que en 1.6. linea.strip() quita el salto de línea final que trae cada línea leída.

5.5 Crear carpetas y listar

Este ejemplo usa una subcarpeta propia para que la lista no dependa de lo que haya escrito antes.

listado = carpeta / "listado"
listado.mkdir(parents=True, exist_ok=True)

(listado / "enero.csv").write_text("a,b\n1,2\n", encoding="utf-8")
(listado / "febrero.csv").write_text("a,b\n3,4\n", encoding="utf-8")
(listado / "notas.txt").write_text("sin datos\n", encoding="utf-8")

print(sorted(x.name for x in listado.iterdir()))       # contenido de la carpeta
print(sorted(x.name for x in listado.glob("*.csv")))   # solo los .csv

Salida esperada:

['enero.csv', 'febrero.csv', 'notas.txt']
['enero.csv', 'febrero.csv']

mkdir(parents=True, exist_ok=True) crea la carpeta (y las intermedias) sin dar error si ya existe. iterdir() lista el contenido; glob("patrón") busca por patrón (*.csv, *.txt, **/*.py con rglob).

Fíjate en (x.name for x in listado.iterdir()): es una expresión generadora, la misma que una lista entre corchetes pero sin construir la lista. Al envolverla en sorted() se convierte en una lista ordenada de nombres. Ordenar es necesario porque iterdir() no garantiza ningún orden.

5.6 Renombrar y borrar

p = carpeta / "listado" / "notas.txt"
p.rename(p.with_name("guardado.txt"))
print("antigua existe:", p.exists())
print("nueva existe:", p.with_name("guardado.txt").exists())

p.with_name("guardado.txt").unlink()   # borra un fichero
print("existe tras unlink:", p.with_name("guardado.txt").exists())

Salida esperada:

antigua existe: False
nueva existe: True
existe tras unlink: False

rename() mueve o renombra; unlink() borra un fichero, y da error si el fichero no existe. Con unlink(missing_ok=True) no da error: es útil cuando no te importa si estaba ahí. with_name(...) te da la misma ruta con otro nombre, sin volver a escribir la carpeta.

Para borrar una carpeta con contenido necesitas shutil.rmtree(carpeta), que viene en el módulo shutil (import shutil) y borra todo lo que hay dentro. Úsalo con mucho cuidado: no hay vuelta atrás.

pathlib no sustituye a open por completo

Para I/O fina (modos w/a, binario, etc.) open() sigue siendo útil; pathlib te da una capa cómoda por encima. Conviven: elige pathlib para rutas y operaciones habituales.

Aplicación práctica: asistente de notas

Un mini-sistema que guarda y recupera notas en JSON:

import json
from pathlib import Path


def guardar(notas, ruta):
    with Path(ruta).open("w", encoding="utf-8") as f:
        json.dump(notas, f, ensure_ascii=False, indent=2)


def cargar(ruta):
    try:
        with Path(ruta).open(encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return []

ruta = carpeta / "notas_demo.json"
ruta.parent.mkdir(parents=True, exist_ok=True)
ruta.unlink(missing_ok=True)  # empieza limpio para que la salida sea estable

notas = cargar(ruta)          # [] porque el fichero no existe todavia
notas.append("Comprar café")
guardar(notas, ruta)
print("Lo que hay en memoria:", notas)
print("Lo que se lee del disco:", cargar(ruta))

Salida esperada:

Lo que hay en memoria: ['Comprar café']
Lo que se lee del disco: ['Comprar café']

Fíjate en la diferencia entre las dos líneas: la primera imprime la lista que tienes en memoria, la segunda vuelve a leer el fichero y devuelve otra lista con el mismo contenido. Que coincidan es la prueba de que la escritura ha funcionado.

La función cargar() ya usa try/except para continuar si el fichero aún no existe, por eso notas vale [] en lugar de fallar. Y unlink(missing_ok=True) borra el fichero si existe, para que una ejecución anterior no cambie la salida. El patrón de excepciones se verá a fondo en 1.11.

Errores frecuentes

  • Olvidar with (o close()) y dejar el fichero abierto o a medias.
  • Usar "w" cuando querías añadir ("w" borra el contenido anterior).
  • No usar encoding="utf-8" y que los acentos se corrompan.
  • Leer un fichero que no existe sin manejar FileNotFoundError.
  • Construir rutas concatenando cadenas (carpeta + "/" + nombre): es fácil olvidar o duplicar separadores y mezclar \ y /; usa pathlib.
  • Usar rutas relativas sin saber desde dónde se ejecuta el programa: se resuelven desde la carpeta actual de la terminal. Ejecuta desde la raíz del proyecto.
  • Llamar a mkdir(parents=False) y fallar si el padre no existe: usa mkdir(parents=True).
  • Llamar a unlink() sobre una carpeta (o a rmdir() sobre una no vacía): unlink es para ficheros y shutil.rmtree para carpetas con contenido.
  • Leer todo el fichero cuando vas a procesarlo línea a línea (memoria).

Práctica de transferencia

  1. Escribe un fichero log.txt con 3 líneas y luego añade una cuarta ("a").
  2. Guarda una lista de diccionarios (2 contactos: nombre/email) en contactos.json y recupérala. Comprueba que la estructura se mantiene.
  3. Con pathlib, crea la estructura practica/ud1_archivos/2026/09/, escribe un fichero en ella y lista su contenido con iterdir().
  4. Usa glob("*.json") para listar los JSON de una carpeta y rglob("*.py") para encontrarlos en subcarpetas.
  5. Amplía el asistente de notas con una función borrar_nota(texto).

Qué debes comprobar: en el punto 1, que el fichero tiene 4 líneas y no 1 (si solo tiene una, has abierto con "w"); en el 2, que lo recuperado es una lista de diccionarios igual a la original (== debe dar True).

Producto evaluable

Cuaderno 08_archivos_log.py con:

  1. El asistente de notas con guardar/cargar, leído antes y después de guardar para comprobar que persiste. Explica también qué ocurre si vuelves a ejecutar el script y no limpias la ruta de demostración.
  2. Una celda que guarde y recupere contactos.json y muestre la estructura.
  3. Una celda con pathlib que cree una estructura de carpetas, escriba un fichero, lo liste con iterdir, lo renombre y lo borre, mostrando el estado en cada paso.

Formato de entrega: cuaderno marimo ejecutado, con salida esperada, interpretación y conclusión.

Resumen y referencia rápida

Tarea Código
Escribir (sobrescribe) with open("ruta.txt", "w", encoding="utf-8") as f: f.write(...)
Leer with open("ruta.txt", encoding="utf-8") as f: texto = f.read()
Añadir al final "a" en open
Línea a línea for linea in f:
JSON json.dump / json.load
Ruta moderna Path("a") / "b" (multiplataforma)
Componentes p.name, p.stem, p.suffix, p.parent
Leer/escribir p.read_text() / p.write_text(...)
Carpetas d.mkdir(parents=True), d.iterdir(), d.glob("*.csv")
Renombrar/borrar p.rename(nuevo), p.unlink()

Siguiente: 1.11 · Excepciones y logging.