AulaML Python · C++
Python intermedio · 42 min · Errores y archivos

Archivos, JSON y CSV

Guardar y recuperar datos en disco sin corromperlos: open, with, encoding, JSON, CSV y pathlib

Los datos que no se guardan desaparecen al cerrar el programa. Aquí aprendes a leer y escribir archivos de texto con open y with, a elegir bien el modo y la codificación, y a convertir entre CSV, JSON y estructuras de Python usando pathlib para las rutas.

Lección 2 de 2 del módulo
Al terminar esta lección podrás
  • Abrir archivos con with, distinguiendo los modos r, w, a y x
  • Elegir la codificación correcta y explicar por qué importa
  • Leer un archivo línea por línea sin cargarlo entero en memoria
  • Convertir un CSV en un informe JSON con json.dump y csv.DictReader
  • Reconocer y corregir los errores de archivo más frecuentes

Antes necesitas: Errores y excepciones

En esta lección
  1. ¿Qué es un archivo?
  2. ¿Para qué sirve guardar datos?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis: open, modos y encoding
  5. Ejemplo básico: escribir y volver a leer
  6. Ejemplo intermedio: leer línea por línea
  7. Caso práctico: de un CSV de ventas a un informe JSON
  8. Visualización: qué ocurre al abrir y cerrar
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. Evaluación

Hasta ahora todos los datos de tus programas han vivido dentro de una ejecución: si cierras la terminal, desaparecen. Un programa real casi siempre necesita lo contrario: guardar resultados para mañana, leer un dataset que preparó otra persona y entregar un informe que cualquiera pueda abrir sin instalar Python.

Esta lección cubre las tres piezas que resuelven casi todos esos casos: el manejo de archivos de texto con open y with, el formato JSON para estructuras de datos y el formato CSV para tablas. Al final sabrás convertir un CSV de ventas en un informe JSON, que es exactamente lo que harás mil veces en un proyecto de datos.

¿Qué es un archivo?

Un archivo es una secuencia de bytes almacenada en un dispositivo, identificada por una ruta. Al abrirlo, el sistema operativo te entrega un flujo: un canal por el que lees o escribes desde una posición llamada cursor. Cuando escribes tres caracteres, el cursor avanza tres posiciones; cuando lees, avanza en el otro sentido. Piensa en un libro con un marcador: el libro existe sin ti, pero el marcador indica por dónde vas.

La extensión no cambia el contenido. Un .csv es texto con comas y saltos de línea; un .json es texto con una sintaxis concreta de llaves y corchetes. Python no adivina el formato: el formato lo decide el programa que lee.

¿Para qué sirve guardar datos?

Estos son los usos que aparecerán una y otra vez en el resto del curso:

  • Persistencia: guardar los resultados de un cálculo para no repetirlo cada vez que arrancas.
  • Intercambio: leer lo que produjo otro programa y devolver algo que otro pueda consumir.
  • Datos de entrenamiento: un datos.csv con una fila por ejemplo y una columna por característica es el punto de partida de la mayoría de modelos.
  • Configuración y métricas: parámetros y resultados en JSON, trazas en texto.
  • Informes: generar un archivo que un jefe o un cliente abra sin saber programar.

¿Cómo funciona por dentro?

Cuando llamas a open(ruta, "r", encoding="utf-8"), Python pide al sistema operativo un descriptor de archivo: un número entero que representa el archivo abierto para tu proceso. Ese descriptor se envuelve en un objeto de Python que, en modo texto, decodifica bytes a str con la codificación indicada y mantiene un búfer para no pedir al disco un byte cada vez.

Escribir tampoco toca el disco de inmediato: f.write(...) copia los datos al búfer, y el volcado real ocurre cuando el búfer se llena, cuando llamas a flush() o cuando cierras el archivo. Por eso un programa que termina sin cerrar puede dejar el archivo a medias. Los descriptores además son un recurso limitado: si abres miles de archivos en un bucle sin cerrarlos, el sistema responde OSError: [Errno 24] Too many open files.

with resuelve ambos problemas usando el protocolo de contexto. Al entrar llama a __enter__, que devuelve el archivo; al salir llama a __exit__, que lo cierra aunque haya una excepción en medio. Es la diferencia entre «normalmente funciona» y «funciona también el día que algo falla».

Sintaxis: open, modos y encoding

PYTHON Las tres formas que usarás el 90 % del tiempo
with open("datos.csv", "w", encoding="utf-8") as f:
    f.write("producto,unidades\nteclado,3\n")

with open("datos.csv", "r", encoding="utf-8") as f:
    texto = f.read()        # todo el contenido en un solo str

with open("salida.txt", "w", encoding="utf-8") as f:
    f.write("primera línea\n")

with open("log.txt", "a", encoding="utf-8") as f:
    f.write("nueva entrada\n")

print(repr(texto))
ModoQué haceQué pasa si el archivo existe
"r"Solo lectura (predeterminado)Lo abre desde el principio
"w"Solo escrituraLo vacía y escribe desde cero
"a"Escritura al final (append)Escribe después de lo que ya había
"x"Escritura exclusivaLanza FileExistsError
"b"Modo binario (se combina: "rb")Devuelve bytes, no str

Si omites encoding, Python usa la codificación regional del sistema: UTF-8 en macOS y Linux, cp1252 en muchas instalaciones de Windows. El mismo archivo con acentos se lee bien en una máquina y con caracteres extraños (ñ) en otra. Escribe siempre encoding="utf-8" y el problema desaparece antes de existir.

Para datos estructurados hay dos módulos de la biblioteca estándar. json trabaja en dos niveles: json.dump(objeto, archivo) y json.load(archivo) con archivos, o json.dumps(objeto) y json.loads(texto) con cadenas. csv ofrece reader/writer para listas y DictReader/DictWriter para diccionarios, que usan la primera fila como cabecera.

Ejemplo básico: escribir y volver a leer

Empecemos por lo mínimo: crear un archivo con dos líneas, cerrarlo y leerlo de nuevo desde el principio.

PYTHON notas.py
# Escribir: "w" crea el archivo o lo vacía si ya existía
with open("notas.txt", "w", encoding="utf-8") as f:
    f.write("Ana: 18\n")
    f.write("Luis: 15\n")

# Leer: sin modo, open usa "r"
with open("notas.txt", encoding="utf-8") as f:
    contenido = f.read()

print(contenido, end="")
print("Caracteres:", len(contenido))

Cuatro detalles que conviene fijar. write no añade saltos de línea: si no pones \n, todo queda en una sola línea. El segundo with abre el archivo en modo lectura y read() devuelve el texto completo. len(contenido) cuenta 17 caracteres, no 2 líneas, porque los saltos también son caracteres. Y print(contenido, end="") evita que print añada un salto extra, ya que el texto ya trae el suyo.

Ejemplo intermedio: leer línea por línea

Un archivo puede ocupar más memoria que la disponible, así que muchas veces no quieres read(). Recorrer el archivo con for pide una línea cada vez y la descarta al avanzar: el consumo es constante aunque el archivo tenga millones de líneas.

PYTHON procesar_notas.py
with open("notas.txt", encoding="utf-8") as f:
    for linea in f:
        # Cada linea termina en \n: hay que limpiarlo antes de separar
        nombre, nota = linea.strip().split(":")
        print(f"{nombre:6} -> {int(nota) + 1} puntos")

strip() quita espacios, tabuladores y saltos de línea de los extremos; sin él, el último campo sería " 18\n". split(":") parte la línea en dos trozos y el desempaquetado los asigna a nombre y nota de una vez. Los valores del archivo son texto: int(nota) es obligatorio para sumar. Y {nombre:6} alinea a la izquierda en seis caracteres para que la salida se lea como una tabla.

Caso práctico: de un CSV de ventas a un informe JSON

Una tienda exporta sus ventas como CSV: una fila por operación, con producto, sede, unidades y precio unitario. El objetivo es agrupar el importe por sede y entregar un JSON que otra área pueda consumir.

PYTHON informe_ventas.py
import csv
import json

filas = """producto,sede,unidades,precio
teclado,Lima,3,120.0
monitor,Lima,1,900.0
teclado,Arequipa,2,120.0
mouse,Lima,5,45.5
"""
# En un caso real el CSV ya existe: aquí lo creamos para poder probarlo
with open("ventas.csv", "w", encoding="utf-8", newline="") as f:
    f.write(filas)

totales = {}
with open("ventas.csv", encoding="utf-8", newline="") as f:
    for fila in csv.DictReader(f):
        importe = int(fila["unidades"]) * float(fila["precio"])
        sede = fila["sede"]
        totales[sede] = totales.get(sede, 0) + importe

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

print(totales)

La salida es {'Lima': 1487.5, 'Arequipa': 240.0} y informe.json contiene lo mismo con dos espacios de sangría. Fíjate en tres decisiones. Primero, newline="" al abrir con el módulo csv: sin él, Windows convierte los saltos y aparecen líneas vacías entre filas. Segundo, DictReader usa la primera fila como cabecera y devuelve un diccionario por fila, pero todos los valores son str: de ahí int() y float(). Tercero, totales.get(sede, 0) porque la primera vez esa sede todavía no existe en el diccionario.

ensure_ascii=False conserva los acentos tal cual en lugar de escaparlos como \u00f1, e indent=2 hace el archivo legible para personas. JSON admite objetos, arrays, cadenas, números, booleanos y null; una tupla se guarda como array y un set lanza TypeError: Object of type set is not JSON serializable. Convierte antes a lista.

Para las rutas, no concatenes cadenas con /: usa pathlib, que funciona igual en los tres sistemas y trae operaciones útiles.

PYTHON rutas.py
from pathlib import Path
import json

carpeta = Path("salidas")
carpeta.mkdir(exist_ok=True)

ruta = carpeta / "informe.json"
ruta.write_text(json.dumps({"ok": True}), encoding="utf-8")

print(ruta.name)
print(ruta.suffix)
print(ruta.exists())
print(ruta.with_suffix(".csv"))

El operador / une rutas sin preocuparse por el separador. read_text y write_text son atajos para archivos pequeños; para archivos grandes sigue usando with open(...). with_suffix cambia la extensión sin tocar el resto de la ruta.

Visualización: qué ocurre al abrir y cerrar

Observa el ciclo completo: abrir en escritura, escribir, cerrar (al salir del with), reabrir en lectura y recorrer las líneas hasta el final del archivo.

Ejecución visual ·Del búfer al disco y de vuelta
1 / 7
1 with open("datos.txt", "w", encoding="utf-8") as f:
2 f.write("Ana,18\nLuis,15\n")
3
4 with open("datos.txt", encoding="utf-8") as f:
5 for linea in f:
6 print(linea.strip())
with llama a open: se crea datos.txt (o se vacía si existía) y el objeto de archivo se asigna a f. Todavía no hay nada escrito en disco.
Variables
f = <archivo 'datos.txt' modo 'w'>
Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Errores comunes

MensajeCausa habitualCómo se arregla
FileNotFoundError: [Errno 2] No such file or directory: 'ventas.csv'La ruta no existe o el programa se ejecuta en otra carpetaComprueba Path(ruta).exists() o usa una ruta absoluta
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 2El archivo se guardó en otra codificación (por ejemplo latin-1)Abre con encoding="latin-1" o convierte el archivo a UTF-8
JSONDecodeError: Expecting value: line 1 column 1 (char 0)El archivo está vacío o no es JSON válidoRevisa el contenido; un JSON vacío no es {}
KeyError: 'unidades'La cabecera del CSV no coincide con la clave que pidesImprime fila la primera vez y compara los nombres
Abrir con "w" dentro del bucle borra lo anterior
Incorrecto
PYTHON Solo queda el último día
dias = ["lunes", "martes", "miércoles"]
for dia in dias:
    with open("diario.txt", "w", encoding="utf-8") as f:
        f.write(dia + "\n")
Correcto
PYTHON Una apertura, todas las líneas
dias = ["lunes", "martes", "miércoles"]
with open("diario.txt", "w", encoding="utf-8") as f:
    for dia in dias:
        f.write(dia + "\n")

Cada `open(..., "w")` trunca el archivo. En el código incorrecto el archivo se vacía tres veces y solo sobrevive `miércoles`. Abre una vez y escribe dentro del `with`; si querías conservar lo anterior, el modo correcto sería `"a"`.

Un error silencioso muy común es envolver la lectura en try/except: pass. Si el archivo no está, el programa sigue con datos vacíos y falla cien líneas después con un mensaje que no explica nada. Mejor decide explícitamente: crear un valor por defecto, o dejar que el FileNotFoundError suba con una ruta clara en el mensaje.

Buenas prácticas

  • Usa siempre with open(...): cierra incluso cuando hay excepciones.
  • Declara encoding="utf-8" en todo archivo de texto; no dependas del sistema.
  • Abre con newline="" cuando el archivo lo lea o escriba el módulo csv.
  • Construye rutas con pathlib.Path, nunca concatenando cadenas con /.
  • Recorre con for linea in f los archivos que puedan ser grandes; reserva read() para archivos pequeños.
  • Valida los datos al entrar: convierte tipos, comprueba columnas y rechaza filas incompletas antes de calcular.

Ejercicios

Ejercicio 1

Diario con resumen en JSON

Crea diario.txt con cuatro líneas, una por día, y vuelve a leerlo línea por línea. Cuenta cuántas líneas tiene, guarda la más larga y escribe ambas cifras en resumen.json con json.dump e indent=2. Muestra el resumen por pantalla.
Pista

Para comparar longitudes usa len(texto) después de quitar el \n con rstrip("\n"). El diccionario que guardas puede tener las claves lineas y mas_larga.

Ejercicio 2

Informe por producto desde un CSV

Parte de un ventas.csv con columnas producto,unidades,precio. Calcula el importe total por producto, guárdalo en informe.json ordenado por clave e imprime el resultado. Si una fila trae unidades vacío, ignórala sin detener el programa.
Pista

csv.DictReader te da cada fila como diccionario. Para ignorar filas usa if not fila["unidades"]: continue. sort_keys=True en json.dump ordena las claves del archivo.

Completar código Completa el guardado y la lectura
import json

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

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

print(recuperado == datos)

`dump` escribe hacia un archivo, `load` lee desde un archivo. Las versiones con `s` trabajan con cadenas.

Soluciones explicadas

En el primer ejercicio, la decisión importante es separar escritura y lectura. Si intentas leer el archivo dentro del mismo with que lo estás escribiendo, el cursor está al final y read() devuelve una cadena vacía. Cerrar y reabrir no es un rodeo: es la garantía de que el búfer se volcó y de que ves lo que realmente quedó en disco.

El segundo ejercicio introduce una idea que reaparecerá al hablar de limpieza de datos: validar en la frontera. El CSV viene de fuera y no puedes confiar en él; convertir unidades a entero sin comprobar antes si está vacío convierte un dato sucio en una excepción que tumba todo el informe. Filtrar la fila mala y seguir suele ser mejor que abortar.

Una alternativa al diccionario acumulador sería collections.defaultdict(float), que evita el get(..., 0). Y si el CSV fuera enorme, podrías escribir el informe con csv.DictWriter en lugar de json.dump para no cargar todos los totales. Ambas variantes son correctas; elige según el tamaño y el consumidor.

Resumen

  • open(ruta, modo, encoding=...) devuelve un objeto de archivo; with lo cierra incluso si hay errores.
  • "r" lee, "w" trunca y escribe, "a" añade al final, "x" falla si ya existe, "b" trabaja con bytes.
  • Declara siempre encoding="utf-8": la codificación por defecto cambia según el sistema.
  • for linea in f recorre sin cargar todo el archivo; cada línea conserva su \n.
  • json.dump/json.load trabajan con archivos; dumps/loads con cadenas. ensure_ascii=False conserva acentos.
  • csv.DictReader usa la primera fila como cabecera y devuelve valores de tipo str: conviértelos.
  • pathlib.Path construye rutas portables y ofrece read_text, write_text, exists y with_suffix.

Evaluación

Evaluación 1

Abres un archivo existente con open("datos.txt", "w"). ¿Qué ocurre en ese instante?

Evaluación 2

¿Por qué with open(...) as f es preferible a f = open(...) seguido de f.close()?

Evaluación 3

Lees un CSV con csv.DictReader y ejecutas int(fila["unidades"]) * 120. La columna contiene "3". ¿Por qué hace falta int()?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.