trubidata
Lección 10 de 12·26 min

Archivos de texto, CSV y JSON

En esta guía (20)

Todo lo que tus programas guardaron hasta ahora vivía en memoria y desaparecía al terminar. En esta lección vas a persistir datos: leer y escribir archivos de texto, trabajar con rutas usando pathlib, y usar dos formatos muy comunes para intercambiar datos: CSV (tablas) y JSON (estructuras con listas y diccionarios). Es lo que vas a usar en el proyecto final para que los gastos no se pierdan.

Abrir un archivo: open y with

Para trabajar con un archivo, primero se abre con open(), indicando la ruta y el modo. La forma correcta de hacerlo es dentro de un bloque with:

python
with open("notas.txt", "w", encoding="utf-8") as archivo:
    archivo.write("Primera línea\n")
    archivo.write("Segunda línea\n")

with open("notas.txt", "r", encoding="utf-8") as archivo:
    contenido = archivo.read()

print(contenido)
text
Primera línea
Segunda línea

  • open(ruta, modo, encoding=...) devuelve un objeto archivo, que as archivo guarda en una variable.
  • with cierra el archivo automáticamente al salir del bloque, incluso si hubo un error. Sin with, tendrías que acordarte de llamar a archivo.close(), y si el programa falla antes, el archivo puede quedar a medio escribir.
  • encoding="utf-8" indica cómo se guardan los caracteres. Ponelo siempre: sin él, Python usa el del sistema operativo y en Windows las tildes y la ñ pueden salir rotas.

Modos

ModoQué haceSi el archivo no existeSi existe
"r"leer (es el modo por defecto)FileNotFoundErrorlo lee
"w"escribirlo crealo borra y escribe desde cero
"a"agregar al finallo creaescribe después de lo que había
"x"crearlo creaFileExistsError

Cuidado con "w": abrir un archivo existente en ese modo borra su contenido en el momento, aunque todavía no escribas nada.

Leer

Hay tres formas, según lo que necesites:

python
with open("notas.txt", encoding="utf-8") as archivo:
    todo = archivo.read()            # todo el contenido en un solo string

with open("notas.txt", encoding="utf-8") as archivo:
    lineas = archivo.readlines()     # lista de líneas, cada una con su \n

with open("notas.txt", encoding="utf-8") as archivo:
    for linea in archivo:            # línea por línea, sin cargar todo en memoria
        print(linea.strip())

print(lineas)
text
Primera línea
Segunda línea
['Primera línea\n', 'Segunda línea\n']

Recorrer el archivo con for es la forma más común: lee de a una línea, así que funciona igual con un archivo de 10 líneas o de 10 millones. Cada línea viene con su \n al final; strip() lo saca.

Escribir y agregar

write escribe un string tal cual: no agrega el salto de línea, tenés que ponerlo vos.

python
tareas = ["comprar pan", "pagar la luz", "llamar al médico"]

with open("tareas.txt", "w", encoding="utf-8") as archivo:
    for tarea in tareas:
        archivo.write(f"{tarea}\n")

with open("tareas.txt", "a", encoding="utf-8") as archivo:
    archivo.write("regar las plantas\n")

with open("tareas.txt", encoding="utf-8") as archivo:
    for numero, linea in enumerate(archivo, start=1):
        print(f"{numero}. {linea.strip()}")
text
1. comprar pan
2. pagar la luz
3. llamar al médico
4. regar las plantas

Rutas con pathlib

En la lección 9 viste Path. Para archivos, tiene atajos muy prácticos:

python
from pathlib import Path

carpeta = Path("salida")
carpeta.mkdir(exist_ok=True)              # crea la carpeta si no existe

ruta = carpeta / "saludo.txt"
ruta.write_text("Hola desde pathlib\n", encoding="utf-8")   # abre, escribe y cierra
print(ruta.read_text(encoding="utf-8").strip())             # abre, lee y cierra
print(ruta.exists())                      # True

for archivo in sorted(carpeta.glob("*.txt")):   # todos los .txt de la carpeta
    print(archivo.name)
text
Hola desde pathlib
True
saludo.txt
  • carpeta / "archivo.txt" arma la ruta con el separador correcto para tu sistema operativo.
  • read_text y write_text son atajos para leer o escribir todo de una vez.
  • mkdir(exist_ok=True) no da error si la carpeta ya existe; con parents=True crea también las carpetas intermedias.
  • glob("*.txt") busca archivos por patrón.
  • ruta.unlink() borra un archivo (con cuidado: no pasa por la papelera).

Rutas relativas: desde dónde corrés el programa

Una ruta como "datos/ventas.csv" es relativa: se busca a partir de la carpeta desde donde ejecutás el programa (Path.cwd()), no desde donde está el archivo .py. Si corrés el script desde otra carpeta, no lo va a encontrar. Para armar rutas relativas al propio script:

python
from pathlib import Path

CARPETA_DEL_SCRIPT = Path(__file__).parent
ruta_datos = CARPETA_DEL_SCRIPT / "datos" / "ventas.csv"

__file__ es una variable que Python define con la ruta del archivo que se está ejecutando.

En código más viejo vas a ver lo mismo escrito con el módulo os:

python
import os

carpeta_del_script = os.path.dirname(os.path.abspath(__file__))
ruta_datos = os.path.join(carpeta_del_script, "datos", "ventas.csv")

Hace exactamente lo mismo; pathlib es más corto y se lee mejor. Evitá armar rutas sumando strings (carpeta + "/ventas.csv"): el separador cambia entre sistemas operativos.

Cuando el archivo no existe

Leer un archivo que no existe lanza FileNotFoundError. Lo manejás como en la lección 8:

python
try:
    with open("no_existe.txt", encoding="utf-8") as archivo:
        print(archivo.read())
except FileNotFoundError:
    print("No encontré el archivo; arranco con datos vacíos")
text
No encontré el archivo; arranco con datos vacíos

También podés preguntar antes con Path("no_existe.txt").exists().

CSV

Un CSV (comma-separated values) es un archivo de texto que representa una tabla: una fila por línea, y los valores separados por comas. La primera fila suele tener los nombres de las columnas:

text
fecha,categoria,descripcion,monto
2026-10-01,comida,supermercado,18500.50
2026-10-02,transporte,carga SUBE,5000
2026-10-03,comida,verdulería,7200

Podrías leerlo con split(","), pero se rompe en cuanto un valor tenga una coma adentro ("Pérez, Ana"). El módulo csv de la biblioteca estándar maneja esos casos.

Escribir con DictWriter

python
import csv

gastos = [
    {"fecha": "2026-10-01", "categoria": "comida", "descripcion": "supermercado", "monto": 18500.50},
    {"fecha": "2026-10-02", "categoria": "transporte", "descripcion": "carga SUBE", "monto": 5000},
    {"fecha": "2026-10-03", "categoria": "comida", "descripcion": "verdulería, frutas", "monto": 7200},
]

with open("gastos.csv", "w", encoding="utf-8", newline="") as archivo:
    escritor = csv.DictWriter(archivo, fieldnames=["fecha", "categoria", "descripcion", "monto"])
    escritor.writeheader()
    escritor.writerows(gastos)

print(open("gastos.csv", encoding="utf-8").read())
text
fecha,categoria,descripcion,monto
2026-10-01,comida,supermercado,18500.5
2026-10-02,transporte,carga SUBE,5000
2026-10-03,comida,"verdulería, frutas",7200

  • DictWriter escribe cada diccionario como una fila, en el orden de fieldnames.
  • newline="" al abrir es necesario para archivos CSV: sin eso, en Windows aparecen líneas en blanco entre filas.
  • Fijate que el valor con coma quedó entre comillas automáticamente.

(El print(open(...).read()) sin with es solo para mostrar el resultado acá; en tu código usá with.)

Leer con DictReader

python
import csv

with open("gastos.csv", encoding="utf-8", newline="") as archivo:
    lector = csv.DictReader(archivo)
    total = 0.0
    for fila in lector:
        monto = float(fila["monto"])
        total += monto
        print(f"{fila['fecha']}  {fila['descripcion']:<20}{monto:>10.2f}")

print(f"Total: {total:.2f}")
text
2026-10-01  supermercado          18500.50
2026-10-02  carga SUBE             5000.00
2026-10-03  verdulería, frutas     7200.00
Total: 30700.50

DictReader usa la primera fila como nombres de columna y devuelve cada fila como un diccionario. Todos los valores llegan como strings, igual que con input(): convertí los números antes de operar.

Si necesitás las filas como listas en vez de diccionarios, existen csv.reader y csv.writer.

JSON

JSON (JavaScript Object Notation) es un formato de texto para guardar estructuras anidadas. Se parece mucho a los diccionarios y listas de Python:

text
{
  "usuario": "ana",
  "moneda": "ARS",
  "categorias": ["comida", "transporte", "ocio"],
  "limite_mensual": 250000,
  "activo": true
}

Es el formato estándar para configuraciones, para intercambiar datos entre programas y para las APIs web. Las equivalencias con Python son directas:

JSONPython
objeto {...}dict
arreglo [...]list
string "..."str
númeroint o float
true / falseTrue / False
nullNone

En JSON las claves siempre son strings y van con comillas dobles.

Guardar y cargar

python
import json

config = {
    "usuario": "ana",
    "moneda": "ARS",
    "categorias": ["comida", "transporte", "ocio"],
    "limite_mensual": 250000,
    "activo": True,
}

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

with open("config.json", encoding="utf-8") as archivo:
    cargado = json.load(archivo)

print(cargado["categorias"][1])     # transporte
print(cargado == config)            # True
  • json.dump(objeto, archivo) escribe; json.load(archivo) lee y devuelve las estructuras de Python.
  • indent=2 lo guarda con sangría, legible para personas.
  • ensure_ascii=False guarda las tildes y la ñ tal cual, en vez de códigos como ñ.

También hay versiones para trabajar con strings en vez de archivos: json.dumps(objeto) devuelve el texto JSON y json.loads(texto) lo convierte a Python.

python
import json

texto = json.dumps({"producto": "té", "precio": 1800}, ensure_ascii=False)
print(texto)               # {"producto": "té", "precio": 1800}
datos = json.loads(texto)
print(datos["precio"] + 200)  # 2000

Un archivo con una lista de registros

Un formato muy común es un objeto JSON con algunos datos generales y una lista de registros adentro. En Python, eso es un diccionario que tiene una lista de diccionarios:

python
import json

agenda = {
    "nombre": "Contactos del club",
    "contactos": [],
}

agenda["contactos"].append({"nombre": "Ana", "apellido": "Sosa", "edad": 29, "cuotas_pagas": [1, 2, 3]})
agenda["contactos"].append({"nombre": "Bruno", "apellido": "Ibarra", "edad": 41, "cuotas_pagas": []})
agenda["contactos"].append({"nombre": "Carla", "apellido": "Medina", "edad": 35, "cuotas_pagas": [1]})

with open("agenda.json", "w", encoding="utf-8") as archivo:
    json.dump(agenda, archivo, indent=4, ensure_ascii=False)

with open("agenda.json", encoding="utf-8") as archivo:
    datos = json.load(archivo)

print(datos["nombre"])
for contacto in datos["contactos"]:
    print(f"- {contacto['nombre']} {contacto['apellido']} ({contacto['edad']}): "
          f"{len(contacto['cuotas_pagas'])} cuotas pagas")
text
Contactos del club
- Ana Sosa (29): 3 cuotas pagas
- Bruno Ibarra (41): 0 cuotas pagas
- Carla Medina (35): 1 cuotas pagas

Un consejo para este tipo de archivos: mantené el mismo tipo en el mismo campo para todos los registros. Si un contacto tiene "cuotas_pagas": [1, 2] y otro "cuotas_pagas": 2, JSON lo acepta sin quejarse, pero el código que lo lee tiene que preguntar cada vez qué tipo vino. Es más simple decidir que siempre es una lista (aunque sea vacía).

Fijate también que una f-string larga se puede partir en dos: dos strings seguidos dentro de un paréntesis se unen solos.

Qué se puede guardar en JSON

Solo diccionarios, listas, strings, números, booleanos y None. Una tupla se guarda como lista (y vuelve como lista). Un set, una fecha (date) u otros objetos dan TypeError: Object of type ... is not JSON serializable: hay que convertirlos antes, por ejemplo una fecha a texto con fecha.isoformat(). Lo vas a hacer en el proyecto final.

JSON mal formado

Si el archivo tiene un error de formato (una coma de más, comillas simples, un archivo cortado), json.load lanza json.JSONDecodeError:

python
import json

try:
    json.loads("{'usuario': 'ana'}")   # comillas simples: no es JSON válido
except json.JSONDecodeError as e:
    print(f"JSON inválido: {e}")
text
JSON inválido: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

CSV o JSON

  • CSV para datos tabulares: todas las filas tienen las mismas columnas y valores simples. Se abre con una planilla de cálculo.
  • JSON para datos anidados o con estructura variable: listas dentro de registros, configuraciones, respuestas de APIs.

Errores comunes

  • Olvidar encoding="utf-8". En Windows, las tildes salen como é o el programa falla con UnicodeDecodeError.
  • Abrir con "w" un archivo que querías conservar. Se borra en el momento. Para agregar, usá "a".
  • No usar with. El archivo puede quedar abierto o a medio escribir si el programa falla.
  • FileNotFoundError con un archivo que existe. Casi siempre es la ruta relativa: estás ejecutando desde otra carpeta. Revisá Path.cwd() o armá la ruta desde Path(__file__).parent.
  • Operar con valores del CSV sin convertir. fila["monto"] + 1 da TypeError: llegan como strings.
  • Olvidar newline="" al escribir CSV. Aparecen filas vacías intercaladas en Windows.
  • Intentar guardar en JSON sets, fechas u objetos propios. Convertilos antes a tipos compatibles.

Practicá

  1. Escribí estadisticas_archivo(ruta: str) -> tuple[int, int, int] que devuelva la cantidad de líneas, palabras y caracteres de un archivo de texto. Probala con un archivo que crees vos.
  2. Con el gastos.csv de esta lección, calculá el total por categoría y escribilo en resumen.csv con las columnas categoria,total, ordenado de mayor a menor total.
  3. Convertí gastos.csv a gastos.json (una lista de diccionarios, con monto como número). Después leé el JSON y mostrá cuántos gastos hay y cuál es el más caro.
  4. Escribí un programa de "anotador": cada vez que se ejecuta, pide una nota y la agrega al final de anotador.txt con la fecha y hora (datetime.now().strftime("%Y-%m-%d %H:%M")), y después muestra todas las notas guardadas.

Soluciones

Ejercicio 1:

python
from pathlib import Path


def estadisticas_archivo(ruta: str) -> tuple[int, int, int]:
    """Devuelve (líneas, palabras, caracteres) de un archivo de texto."""
    texto = Path(ruta).read_text(encoding="utf-8")
    return len(texto.splitlines()), len(texto.split()), len(texto)


Path("prueba.txt").write_text("hola mundo\nesto es una prueba\n", encoding="utf-8")
lineas, palabras, caracteres = estadisticas_archivo("prueba.txt")
print(f"{lineas} líneas, {palabras} palabras, {caracteres} caracteres")
text
2 líneas, 6 palabras, 30 caracteres

Ejercicio 2:

python
import csv

totales: dict[str, float] = {}
with open("gastos.csv", encoding="utf-8", newline="") as archivo:
    for fila in csv.DictReader(archivo):
        categoria = fila["categoria"]
        totales[categoria] = totales.get(categoria, 0) + float(fila["monto"])

ordenadas = sorted(totales.items(), key=lambda par: par[1], reverse=True)

with open("resumen.csv", "w", encoding="utf-8", newline="") as archivo:
    escritor = csv.writer(archivo)
    escritor.writerow(["categoria", "total"])
    for categoria, total in ordenadas:
        escritor.writerow([categoria, f"{total:.2f}"])

print(open("resumen.csv", encoding="utf-8").read())
text
categoria,total
comida,25700.50
transporte,5000.00

sorted(..., key=...) ordena según lo que devuelva la función key para cada elemento. lambda par: par[1] es una función anónima de una línea: recibe par (una tupla (categoria, total)) y devuelve par[1], el total. Es equivalente a definir def por_total(par): return par[1] y pasar key=por_total.

Ejercicio 3:

python
import csv
import json

with open("gastos.csv", encoding="utf-8", newline="") as archivo:
    gastos = []
    for fila in csv.DictReader(archivo):
        fila["monto"] = float(fila["monto"])
        gastos.append(fila)

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

with open("gastos.json", encoding="utf-8") as archivo:
    cargados = json.load(archivo)

mas_caro = max(cargados, key=lambda g: g["monto"])
print(f"{len(cargados)} gastos")
print(f"Más caro: {mas_caro['descripcion']} (${mas_caro['monto']:.2f})")
text
3 gastos
Más caro: supermercado ($18500.50)

max también acepta key: devuelve el elemento cuyo key es mayor.

Ejercicio 4:

python
from datetime import datetime
from pathlib import Path

RUTA = Path("anotador.txt")

nota = input("Nueva nota: ").strip()
if nota:
    momento = datetime.now().strftime("%Y-%m-%d %H:%M")
    with open(RUTA, "a", encoding="utf-8") as archivo:
        archivo.write(f"[{momento}] {nota}\n")

print("\nNotas guardadas:")
if RUTA.exists():
    print(RUTA.read_text(encoding="utf-8"), end="")
else:
    print("(ninguna)")
text
Nueva nota: comprar lamparitas

Notas guardadas:
[2026-10-13 18:30] comprar lamparitas

Cada ejecución agrega una línea, así que las notas se acumulan entre ejecuciones: el modo "a" es justamente para eso.

Resumen

  • Abrí archivos con with open(ruta, modo, encoding="utf-8") as f:; with los cierra solo.
  • "r" lee, "w" crea o pisa, "a" agrega al final.
  • read() trae todo, for linea in archivo recorre línea por línea; write() no agrega \n.
  • pathlib.Path arma rutas con /, crea carpetas, busca archivos y tiene read_text/write_text.
  • Las rutas relativas dependen de desde dónde ejecutás; Path(__file__).parent da la carpeta del script.
  • csv.DictReader y csv.DictWriter leen y escriben tablas como diccionarios; los valores llegan como strings; abrí con newline="".
  • json.dump/json.load guardan y cargan diccionarios y listas; usá indent=2 y ensure_ascii=False.

← Volver al temario