Archivos en Python: leer y escribir texto, CSV y JSON

📅 Actualizado en marzo 2026 📊 Nivel: Principiante ⏱️ 18 min de lectura

Todo lo que tu programa calcula desaparece cuando el proceso termina. Si quieres que los datos sobrevivan entre ejecuciones — una configuración, los resultados de un análisis, un registro de actividad — necesitas guardarlos en algún sitio. Los archivos son la forma más directa: están ahí cuando el programa termina, cuando reinicias el ordenador y cuando abres el resultado en otro programa.

Python tiene soporte nativo para tres formatos que cubren el 90% de los casos: texto plano, CSV y JSON. No necesitas instalar nada.

📂 open() y with: abrir archivos de forma segura

La función open() devuelve un objeto archivo. Lo más importante: siempre usar with, que garantiza que el archivo se cierra aunque ocurra una excepción.

# La forma correcta — siempre
with open("notas.txt", "r", encoding="utf-8") as f:
    contenido = f.read()
# Aquí f ya está cerrado automáticamente

# Sin with (funciona pero es propenso a errores):
f = open("notas.txt", "r", encoding="utf-8")
try:
    contenido = f.read()
finally:
    f.close()    # hay que acordarse siempre

# Verificar si un archivo existe antes de abrirlo:
from pathlib import Path

ruta = Path("config.json")
if ruta.exists():
    with open(ruta, encoding="utf-8") as f:
        datos = f.read()
else:
    print("No hay configuración, usando valores por defecto.")
Diagrama del ciclo de vida de un archivo en Python: modos de apertura, operaciones permitidas y cierre automático con with
Ciclo de vida de un archivo: open() con su modo determina qué operaciones están disponibles. El rombo de decisión separa las tres ramas — lectura, escritura y ambas. El with garantiza el cierre en todos los casos. Infografía: Ciberaula.

📖 Leer archivos de texto

## read() — todo el contenido como un string
with open("poema.txt", encoding="utf-8") as f:
    texto = f.read()
print(texto)
print(f"Total: {len(texto)} caracteres")

## readline() — una línea cada vez
with open("poema.txt", encoding="utf-8") as f:
    primera = f.readline()     # "En el principio era el Verbo\n"
    segunda = f.readline()     # siguiente línea
print(primera.strip())

## Iteración directa — la más eficiente en memoria
with open("accesos.log", encoding="utf-8") as f:
    for linea in f:
        linea = linea.rstrip("\n")   # quitar salto de línea
        if "ERROR" in linea:
            print(linea)

## readlines() — lista de todas las líneas
with open("datos.txt", encoding="utf-8") as f:
    lineas = f.readlines()
# ['línea 1\n', 'línea 2\n', 'línea 3\n']

# Limpiar los saltos de línea de golpe:
lineas = [l.rstrip() for l in lineas]
Vinilos de música ordenados alfabéticamente con etiquetas en una tienda de discos
Cada disco tiene una posición y un nombre para encontrarlo. Los archivos en disco funcionan igual: una ruta única que los identifica y permite leerlos o modificarlos. Fuente: Pexels (licencia libre).

✏️ Escribir y añadir contenido

## write() — modo "w" crea o sobreescribe
with open("informe.txt", "w", encoding="utf-8") as f:
    f.write("Informe de ventas\n")
    f.write("=================\n")
    f.write(f"Total: {1234} €\n")

## writelines() — escribe una lista (sin añadir \n automático)
lineas = ["Producto A: 100\n", "Producto B: 200\n"]
with open("productos.txt", "w", encoding="utf-8") as f:
    f.writelines(lineas)

## Modo "a" — append: añade sin borrar lo anterior
import datetime
with open("app.log", "a", encoding="utf-8") as f:
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    f.write(f"[{timestamp}] Aplicación iniciada\n")

## Modo "x" — creación exclusiva (error si ya existe)
try:
    with open("nuevo_archivo.txt", "x", encoding="utf-8") as f:
        f.write("Creado por primera vez\n")
except FileExistsError:
    print("El archivo ya existía, no se sobreescribió.")

🗂️ pathlib: rutas como objetos

pathlib.Path es la forma moderna de trabajar con rutas. En lugar de strings y os.path, tienes un objeto con métodos propios que funciona igual en Windows, macOS y Linux.

from pathlib import Path

# Crear rutas (/ funciona en todos los sistemas operativos)
base    = Path("datos")
archivo = base / "usuarios" / "lista.csv"
# En Windows: datos\usuarios\lista.csv
# En Unix:    datos/usuarios/lista.csv

# Propiedades útiles
p = Path("docs/informe_2024.pdf")
p.name        # "informe_2024.pdf"
p.stem        # "informe_2024"  (sin extensión)
p.suffix      # ".pdf"
p.parent      # Path("docs")
p.exists()    # True / False
p.is_file()   # True si es archivo
p.is_dir()    # True si es directorio

# Leer y escribir directamente (sin open())
texto = Path("notas.txt").read_text(encoding="utf-8")
Path("salida.txt").write_text("Contenido", encoding="utf-8")

# Crear directorio (con padres si hace falta)
Path("datos/cache/tmp").mkdir(parents=True, exist_ok=True)

# Listar archivos de un directorio
for archivo in Path("datos").glob("*.csv"):
    print(archivo.name)

# Ruta absoluta
print(Path("config.json").resolve())
# /home/ana/proyecto/config.json

📊 CSV: datos tabulares

El módulo csv viene con Python. DictReader y DictWriter son las formas más cómodas: usan la primera fila como cabecera y trabajan con diccionarios por fila.

import csv

## Leer — DictReader (cabecera automática)
with open("empleados.csv", encoding="utf-8") as f:
    lector = csv.DictReader(f)
    for fila in lector:
        print(f"{fila['nombre']} — {fila['departamento']}")

## Leer — csv.reader (sin cabecera, devuelve listas)
with open("numeros.csv", encoding="utf-8") as f:
    lector = csv.reader(f)
    next(lector)    # saltar cabecera manualmente
    for fila in lector:
        print(fila[0], fila[1])

## Escribir — DictWriter
empleados = [
    {"nombre": "Ana",  "departamento": "IT",    "salario": 38000},
    {"nombre": "Luis", "departamento": "Ventas", "salario": 32000},
]
campos = ["nombre", "departamento", "salario"]

with open("salida.csv", "w", newline="", encoding="utf-8") as f:
    escritor = csv.DictWriter(f, fieldnames=campos)
    escritor.writeheader()      # escribe la fila de cabecera
    escritor.writerows(empleados)

## CSV con separador punto y coma (formato europeo)
with open("europeo.csv", encoding="utf-8") as f:
    lector = csv.DictReader(f, delimiter=";")
    for fila in lector:
        print(fila)
Cuaderno de espiral abierto en blanco con lápiz negro sobre mesa blanca
Abrir un archivo en modo escritura es como abrir un cuaderno en blanco: puedes escribir lo que necesites y cerrarlo cuando termines. El modo "a" sería añadir páginas al final sin borrar las anteriores. Fuente: Pexels (licencia libre).

🔧 JSON: configuraciones y APIs

JSON es el formato estándar para configuraciones, respuestas de APIs REST y persistencia ligera de datos. Python lo convierte directamente a y desde diccionarios, listas, strings, números y booleanos.

import json

## Leer JSON desde archivo
with open("config.json", encoding="utf-8") as f:
    config = json.load(f)

print(config["base_de_datos"]["host"])

## Escribir JSON en archivo
config = {
    "version": "1.2",
    "base_de_datos": {
        "host": "localhost",
        "puerto": 5432,
        "nombre": "mi_app"
    },
    "debug": False,
    "etiquetas": ["produccion", "v2"]
}

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

## String ↔ dict (sin archivo — útil para APIs)
texto_json = '{"nombre": "Ana", "edad": 28}'
datos = json.loads(texto_json)       # str  → dict
print(datos["nombre"])               # "Ana"

cadena = json.dumps(datos, indent=2) # dict → str
print(cadena)

## JSON con tipos Python especiales
from datetime import date
import json

# Las fechas no son JSON nativas — necesitas convertirlas:
class EncoderFechas(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, date):
            return obj.isoformat()    # "2026-03-06"
        return super().default(obj)

datos = {"nombre": "Ana", "nacimiento": date(1995, 4, 12)}
json.dumps(datos, cls=EncoderFechas)
# '{"nombre": "Ana", "nacimiento": "1995-04-12"}'
Referencia rápida de archivos en Python: texto, CSV y JSON con sus métodos principales y advertencias
Referencia rápida — Texto, CSV y JSON en tres columnas: apertura, lectura, escritura, métodos principales y advertencias frecuentes. Incluye pathlib como alternativa moderna a open(). Ficha: Ciberaula.

🛠️ Programa completo: agenda en JSON

import json
from pathlib import Path
from datetime import datetime

ARCHIVO = Path("agenda.json")

def cargar_agenda():
    if not ARCHIVO.exists():
        return []
    try:
        return json.loads(ARCHIVO.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        print("⚠ El archivo de agenda está corrupto. Empezando desde cero.")
        return []

def guardar_agenda(contactos):
    ARCHIVO.write_text(
        json.dumps(contactos, indent=2, ensure_ascii=False),
        encoding="utf-8"
    )

def mostrar_contactos(contactos):
    if not contactos:
        print("  (agenda vacía)")
        return
    for i, c in enumerate(sorted(contactos, key=lambda x: x["nombre"]), 1):
        print(f"  {i:>2}. {c['nombre']:<25} {c.get('telefono','—')}")

def buscar(contactos, texto):
    texto = texto.lower()
    return [c for c in contactos
            if texto in c["nombre"].lower()
            or texto in c.get("email","").lower()]

# ── Programa principal ──
contactos = cargar_agenda()
print(f"Agenda cargada: {len(contactos)} contacto(s)\n")

while True:
    print("1. Ver todos  2. Buscar  3. Añadir  4. Eliminar  5. Salir")
    op = input("Opción: ").strip()

    if op == "1":
        mostrar_contactos(contactos)

    elif op == "2":
        texto = input("  Buscar: ").strip()
        resultado = buscar(contactos, texto)
        print(f"  {len(resultado)} resultado(s):")
        mostrar_contactos(resultado)

    elif op == "3":
        nombre = input("  Nombre: ").strip()
        telefono = input("  Teléfono: ").strip()
        email = input("  Email: ").strip()
        contactos.append({
            "nombre": nombre,
            "telefono": telefono,
            "email": email,
            "creado": datetime.now().isoformat()
        })
        guardar_agenda(contactos)
        print(f"  ✓ {nombre} añadido y guardado.")

    elif op == "4":
        mostrar_contactos(contactos)
        try:
            idx = int(input("  Nº a eliminar: ")) - 1
            eliminado = sorted(contactos, key=lambda x: x["nombre"])[idx]
            contactos.remove(eliminado)
            guardar_agenda(contactos)
            print(f"  ✓ {eliminado['nombre']} eliminado.")
        except (ValueError, IndexError):
            print("  Número no válido.")

    elif op == "5":
        print("Hasta luego.")
        break

🐛 Errores clásicos con archivos

1. Olvidar encoding y romper las tildes

# ❌ Depende del sistema — falla en Windows con tildes
with open("texto.txt") as f:
    contenido = f.read()

# ✅ Siempre explícito
with open("texto.txt", encoding="utf-8") as f:
    contenido = f.read()

2. Usar "w" cuando querías "a"

# ❌ Borra el log anterior en cada ejecución
with open("app.log", "w") as f:
    f.write("Nueva entrada\n")

# ✅ Añade sin borrar
with open("app.log", "a") as f:
    f.write("Nueva entrada\n")

3. Olvidar newline="" en CSV en Windows

# ❌ Genera líneas en blanco entre filas en Windows
with open("datos.csv", "w") as f:
    writer = csv.writer(f)

# ✅
with open("datos.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)

4. Confundir json.load con json.loads

cadena = '{"clave": "valor"}'

# ❌
datos = json.load(cadena)       # TypeError: espera un archivo, no string

# ✅
datos = json.loads(cadena)      # "s" = string
with open("f.json") as f:
    datos = json.load(f)        # sin "s" = objeto archivo

✅ Resumen y próximos pasos

Usa siempre with open() y especifica encoding="utf-8". El modo "r" lee, "w" crea o sobreescribe, "a" añade, "x" crea solo si no existe. Para datos tabulares, csv.DictReader y DictWriter. Para configuraciones y APIs, json.load y json.dump. Y pathlib.Path para trabajar con rutas de forma portable y expresiva.

La siguiente lección: módulos y paquetes — cómo organizar el código en varios archivos, importar módulos estándar y trabajar con pip.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Archivos en Python: leer y escribir texto, CSV y JSON

Las dudas más comunes respondidas de forma clara y directa.

El modo "w" trunca el archivo al abrirlo: borra todo el contenido existente y empieza desde cero. Si quieres añadir contenido sin perder lo anterior, usa "a" (append). Si quieres escribir pero solo si el archivo no existe aún, usa "x" que lanza FileExistsError si ya existe.
Si no especificas encoding, Python usa la codificación por defecto del sistema operativo, que en Windows suele ser cp1252 o latin-1. Un archivo guardado en UTF-8 leído con latin-1 corrompe las tildes y la ñ. Especificar encoding="utf-8" explícitamente hace el código portable: funcionará igual en Windows, macOS y Linux.
readlines() carga todas las líneas en memoria como una lista. Si el archivo tiene millones de líneas, eso puede agotar la RAM. La iteración directa con for linea in f lee línea a línea sin cargar todo el archivo, que es mucho más eficiente en memoria. Usa readlines() solo cuando necesites acceder a las líneas en orden aleatorio o por índice. Para procesar secuencialmente, siempre itera.
json.load(f) lee desde un objeto archivo ya abierto. json.loads(cadena) parsea una cadena de texto. La "s" final significa "string". Del mismo modo, json.dump(datos, f) escribe en un archivo y json.dumps(datos) devuelve una cadena. El patrón es: load/dump para archivos, loads/dumps para strings.
Sí, en la práctica casi siempre. pathlib.Path representa rutas como objetos con métodos propios: path.exists(), path.read_text(), path.stem, path.parent. El código es más legible y menos propenso a errores que concatenar strings con os.path.join(). os.path sigue siendo válido y encontrarás mucho código que lo usa, pero para código nuevo pathlib es la recomendación actual.
En Windows, el modo texto traduce "\n" a "\r\n" (dos caracteres). El módulo csv ya añade sus propios saltos de línea, así que sin newline="" cada fila terminaría con "\r\r\n" y el archivo tendría líneas en blanco entre cada fila. El parámetro newline="" desactiva esa traducción automática y deja al módulo csv gestionar los finales de línea correctamente.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Archivos en Python: leer y escribir texto, CSV y JSON? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!