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:
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)
Primera línea
Segunda línea
open(ruta, modo, encoding=...)devuelve un objeto archivo, queas archivoguarda en una variable.withcierra el archivo automáticamente al salir del bloque, incluso si hubo un error. Sinwith, tendrías que acordarte de llamar aarchivo.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
| Modo | Qué hace | Si el archivo no existe | Si existe |
|---|---|---|---|
"r" | leer (es el modo por defecto) | FileNotFoundError | lo lee |
"w" | escribir | lo crea | lo borra y escribe desde cero |
"a" | agregar al final | lo crea | escribe después de lo que había |
"x" | crear | lo crea | FileExistsError |
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:
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)
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.
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()}")
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:
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)
Hola desde pathlib
True
saludo.txt
carpeta / "archivo.txt"arma la ruta con el separador correcto para tu sistema operativo.read_textywrite_textson atajos para leer o escribir todo de una vez.mkdir(exist_ok=True)no da error si la carpeta ya existe; conparents=Truecrea 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:
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:
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:
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")
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:
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
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())
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
DictWriterescribe cada diccionario como una fila, en el orden defieldnames.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
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}")
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:
{
"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:
| JSON | Python |
|---|---|
objeto {...} | dict |
arreglo [...] | list |
string "..." | str |
| número | int o float |
true / false | True / False |
null | None |
En JSON las claves siempre son strings y van con comillas dobles.
Guardar y cargar
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=2lo guarda con sangría, legible para personas.ensure_ascii=Falseguarda 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.
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:
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")
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:
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}")
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 conUnicodeDecodeError. - 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. FileNotFoundErrorcon un archivo que existe. Casi siempre es la ruta relativa: estás ejecutando desde otra carpeta. RevisáPath.cwd()o armá la ruta desdePath(__file__).parent.- Operar con valores del CSV sin convertir.
fila["monto"] + 1daTypeError: 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á
- 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. - Con el
gastos.csvde esta lección, calculá el total por categoría y escribilo enresumen.csvcon las columnascategoria,total, ordenado de mayor a menor total. - Convertí
gastos.csvagastos.json(una lista de diccionarios, conmontocomo número). Después leé el JSON y mostrá cuántos gastos hay y cuál es el más caro. - Escribí un programa de "anotador": cada vez que se ejecuta, pide una nota y la agrega al final de
anotador.txtcon la fecha y hora (datetime.now().strftime("%Y-%m-%d %H:%M")), y después muestra todas las notas guardadas.
Soluciones
Ejercicio 1:
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")
2 líneas, 6 palabras, 30 caracteres
Ejercicio 2:
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())
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:
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})")
3 gastos
Más caro: supermercado ($18500.50)
max también acepta key: devuelve el elemento cuyo key es mayor.
Ejercicio 4:
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)")
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:;withlos cierra solo. "r"lee,"w"crea o pisa,"a"agrega al final.read()trae todo,for linea in archivorecorre línea por línea;write()no agrega\n.pathlib.Patharma rutas con/, crea carpetas, busca archivos y tieneread_text/write_text.- Las rutas relativas dependen de desde dónde ejecutás;
Path(__file__).parentda la carpeta del script. csv.DictReaderycsv.DictWriterleen y escriben tablas como diccionarios; los valores llegan como strings; abrí connewline="".json.dump/json.loadguardan y cargan diccionarios y listas; usáindent=2yensure_ascii=False.