Serie Fundamentos de Python — Parte 8. Esta publicación continúa la Parte 1: Lo que Todo Principiante Debería Saber, la Parte 2: Variables y Tipos de Datos, la Parte 3: Listas y Diccionarios, la Parte 4: La Sentencia If, la Parte 5: Entrada del Usuario y Ciclos While, la Parte 6: Funciones y la Parte 7: Clases. Cada publicación de la serie toma un tema y lo explica a fondo. Esta trata sobre archivos y excepciones.

Todos los programas de esta serie han tenido hasta ahora dos debilidades. Primero, olvidan todo al terminar: los proyectos que ingresaste, las tareas que agregaste y las cotizaciones que calculaste desaparecen en cuanto el programa se detiene. Segundo, son frágiles: un solo valor inesperado (una palabra donde iba un número, una clave que falta, un archivo que no existe) y el programa se detiene con un error.

Esta publicación resuelve ambas cosas. Aprenderás a guardar datos en archivos y a cargarlos de nuevo, incluidos los formatos CSV y JSON que usan las hojas de cálculo y las APIs. Y aprenderás a manejar excepciones, los errores de Python, para que tus programas puedan recuperarse de los problemas en lugar de detenerse. En la Parte 5 viste un primer vistazo a try y except; aquí verás el panorama completo.

Ejecuta estos ejemplos como script. Guarda cada uno en un archivo .py y ejecútalo con python3 tu_archivo.py. Los archivos se crean en la carpeta desde la que ejecutas el comando, así que crea una carpeta nueva para practicar y ejecuta tus scripts desde ahí.

Escribir en un Archivo

Para trabajar con un archivo, lo abres con la función integrada open(). La forma más limpia es con una sentencia with, que cierra el archivo automáticamente cuando termina el bloque, incluso si ocurre un error dentro de él:

with open("notes.txt", "w", encoding="utf-8") as file:
    file.write("Call Bright Bakery about the menu page.\n")
    file.write("Send invoice to Valley Dental.\n")
    file.write("Back up client files.\n")

Esa primera línea tiene mucho:

  • "notes.txt" es el nombre del archivo. Como no es una ruta completa, Python lo busca en la carpeta actual.
  • "w" es el modo: escritura (write). Crea el archivo si no existe y borra todo su contenido si ya existe.
  • encoding="utf-8" le indica a Python cómo guardar el texto. UTF-8 admite todos los idiomas y símbolos (como ñ, é o ✓) y es el estándar en la web. Inclúyelo siempre, para que tus archivos funcionen igual en cualquier computadora.
  • as file le da al archivo abierto un nombre que puedes usar dentro del bloque.

file.write() escribe exactamente el texto que le das; no agrega un salto de línea como hace print(). Por eso cada línea termina con \n, el carácter de salto de línea.

Agregar a un Archivo en Lugar de Reemplazarlo

Para agregar líneas al final de un archivo existente, ábrelo en modo anexar (append), "a":

with open("notes.txt", "a", encoding="utf-8") as file:
    file.write("Renew the domain for Calexico Coffee.\n")

Modos de Archivo

ModoNombreQué hace
"r"LecturaAbre un archivo existente para leerlo. Es el modo predeterminado. Da error si el archivo no existe.
"w"EscrituraCrea el archivo, o lo borra si ya existe, y luego escribe.
"a"AnexarCrea el archivo si hace falta y luego agrega al final.
"x"CrearCrea un archivo nuevo. Da error si el archivo ya existe, así que no puedes sobrescribir por accidente.

Leer un Archivo

Para leer el archivo completo de una vez, ábrelo en modo lectura ("r", o sin indicar el modo) y llama a .read():

with open("notes.txt", encoding="utf-8") as file:
    contents = file.read()

print(contents)
Call Bright Bakery about the menu page.
Send invoice to Valley Dental.
Back up client files.
Renew the domain for Calexico Coffee.

Fíjate en la línea vacía al final: la última línea del archivo termina con \n, y print() agrega otro.

Leer Línea por Línea

Lo más común es que quieras trabajar con una línea a la vez. Recorre el archivo directamente:

with open("notes.txt", encoding="utf-8") as file:
    for number, line in enumerate(file, start=1):
        print(f"{number}. {line.strip()}")
1. Call Bright Bakery about the menu page.
2. Send invoice to Valley Dental.
3. Back up client files.
4. Renew the domain for Calexico Coffee.

Cada línea todavía incluye su \n al final, así que llama a .strip() para quitarlo (si no, tendrías una línea vacía extra entre cada una). Esta forma también funciona con archivos enormes, porque Python lee una línea a la vez en lugar de cargar todo el archivo en memoria.

Para obtener todas las líneas como una lista, usa .read().splitlines(), que además quita los saltos de línea:

with open("notes.txt", encoding="utf-8") as file:
    tasks = file.read().splitlines()

print(len(tasks))   # 4
print(tasks[0])     # Call Bright Bakery about the menu page.

Los Números en los Archivos Son Texto

Todo lo que lees de un archivo de texto es una cadena, igual que con input() en la Parte 5. Convierte los números antes de hacer operaciones con ellos:

with open("visits.txt", "w", encoding="utf-8") as file:
    file.write("1200\n340\n210\n")

with open("visits.txt", encoding="utf-8") as file:
    visits = [int(line) for line in file]

print(sum(visits))   # 1750

int() ignora los espacios y el salto de línea alrededor de un número, así que int("1200\n") funciona sin llamar antes a .strip().

Trabajar con Rutas: pathlib

El módulo pathlib te da una forma cómoda de trabajar con archivos y carpetas. Un objeto Path representa una ubicación en tu computadora e incluye métodos útiles:

from pathlib import Path

path = Path("notes.txt")

print(path.exists())   # True
print(path.suffix)     # .txt
print(path.stem)       # notes

text = path.read_text(encoding="utf-8")       # lee todo el archivo en una línea
Path("summary.txt").write_text("4 tasks\n", encoding="utf-8")   # escribe en una línea

Puedes construir rutas con /, y Python usa el separador correcto en Windows, macOS y Linux:

from pathlib import Path

reports = Path("reports")
reports.mkdir(exist_ok=True)              # crea la carpeta si no existe
report_file = reports / "january.txt"     # reports/january.txt
report_file.write_text("Revenue: $5,780\n", encoding="utf-8")

for txt_file in Path(".").glob("*.txt"):   # cada archivo .txt de la carpeta actual
    print(txt_file.name)

read_text() y write_text() son perfectos para archivos pequeños. Para leer línea por línea o para anexar, usa open() como viste antes; los objetos Path también funcionan con open().

Archivos CSV: Datos de Hojas de Cálculo

CSV (valores separados por comas) es la forma más sencilla de intercambiar datos con hojas de cálculo: Excel y Google Sheets pueden abrirlo y exportarlo. Cada línea es una fila, y las comas separan las columnas. El módulo integrado csv de Python se encarga de los detalles, como los valores que contienen comas.

Las herramientas más cómodas son DictWriter y DictReader, que trabajan con las listas de diccionarios de la Parte 3:

import csv

projects = [
    {"client": "Bright Bakery", "type": "Website", "budget": 2500},
    {"client": "Lopez Landscaping", "type": "SEO", "budget": 900},
    {"client": "Calexico Coffee", "type": "Website", "budget": 3200},
]

with open("projects.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(file, fieldnames=["client", "type", "budget"])
    writer.writeheader()
    writer.writerows(projects)

Eso crea un archivo que puedes abrir en cualquier programa de hojas de cálculo:

client,type,budget
Bright Bakery,Website,2500
Lopez Landscaping,SEO,900
Calexico Coffee,Website,3200

Al leerlo de nuevo, obtienes un diccionario por fila, con la fila de encabezados como claves:

import csv

with open("projects.csv", newline="", encoding="utf-8") as file:
    rows = list(csv.DictReader(file))

print(rows[0])   # {'client': 'Bright Bakery', 'type': 'Website', 'budget': '2500'}

total = sum(int(row["budget"]) for row in rows)
print(total)     # 6600

Dos detalles que debes recordar: pasa siempre newline="" al abrir archivos CSV (el módulo csv maneja los saltos de línea por su cuenta), y cada valor regresa como cadena (fíjate en '2500'), así que convierte los números antes de usarlos.

Archivos JSON: Guardar Datos de Python

JSON es el formato que usan la mayoría de las APIs, y corresponde casi perfectamente a las listas y los diccionarios de Python. Eso lo convierte en la forma más fácil de guardar los datos de tu programa y cargarlos después, con los números y los valores True/False intactos:

import json

settings = {
    "business": "Bright Bakery",
    "hourly_rate": 85,
    "rush_fee": 0.25,
    "services": ["Website", "SEO", "Hosting"],
    "accepting_clients": True,
}

with open("settings.json", "w", encoding="utf-8") as file:
    json.dump(settings, file, indent=2)

with open("settings.json", encoding="utf-8") as file:
    loaded = json.load(file)

print(loaded["hourly_rate"] * 2)   # 170   (¡sigue siendo un número!)
print(loaded["services"][0])       # Website
  • json.dump(datos, archivo) escribe datos de Python en un archivo. indent=2 les da un formato ordenado para que también las personas puedan leerlos.
  • json.load(archivo) los lee de nuevo como listas y diccionarios de Python.

(No los confundas con json.dumps() y json.loads() de la Parte 3: la s extra significa string, cadena. Esas funciones convierten hacia y desde una cadena en lugar de un archivo.)

¿Qué Es una Excepción?

Cuando algo sale mal mientras un programa se ejecuta, Python lanza una excepción: un objeto de error que describe lo que pasó. Si nada la maneja, el programa se detiene e imprime un traceback (rastreo):

prices = {"Basic": 499, "Business": 1499}
print(prices["Premium"])
Traceback (most recent call last):
  File "/Users/you/practice/prices.py", line 2, in <module>
    print(prices["Premium"])
          ~~~~~~^^^^^^^^^^^
KeyError: 'Premium'

Lee un traceback de abajo hacia arriba. La última línea te dice el tipo de excepción (KeyError) y un mensaje ('Premium'). Las líneas de arriba muestran dónde ocurrió: el archivo, el número de línea y el código de esa línea.

Ya conociste varios tipos de excepciones en esta serie. Estos son los que los principiantes encuentran con más frecuencia:

ExcepciónCuándo ocurreEjemplo
ValueErrorUn valor tiene el tipo correcto pero un contenido incorrectoint("five")
TypeErrorUna operación recibe el tipo equivocado"Pages: " + 5
KeyErrorUna clave de diccionario no existeclient["phone"]
IndexErrorUn índice de lista está fuera de rangopages[10]
ZeroDivisionErrorDividir entre cerototal / 0
FileNotFoundErrorAbrir un archivo que no existeopen("missing.txt")
NameErrorUsar una variable que nunca se definióprint(totl)

Manejar Excepciones con try y except

Para manejar una excepción, pon el código que podría fallar en un bloque try, y el código que debe ejecutarse si falla en un bloque except:

try:
    with open("clients.txt", encoding="utf-8") as file:
        print(file.read())
except FileNotFoundError:
    print("No clients yet. Add your first client to get started.")

Si clients.txt existe, se imprime su contenido y se salta el bloque except. Si no existe, Python salta al bloque except en lugar de detenerse con un error, y el programa sigue funcionando.

Atrapa Excepciones Específicas

Indica siempre la excepción que esperas. Cada problema merece una respuesta distinta, y puedes tener varios bloques except:

def average_budget(total, count_text):
    try:
        count = int(count_text)
        return total / count
    except ValueError:
        print(f"'{count_text}' isn't a whole number.")
    except ZeroDivisionError:
        print("Can't average zero projects.")
    return None

print(average_budget(6600, "3"))     # 2200.0
print(average_budget(6600, "three"))
print(average_budget(6600, "0"))
2200.0
'three' isn't a whole number.
None
Can't average zero projects.
None

Obtener el Mensaje de Error con as

Agrega as y un nombre para obtener el propio objeto de la excepción. Al imprimirlo, ves el mensaje de Python:

try:
    pages = int("five")
except ValueError as error:
    print(f"Couldn't convert: {error}")
# Couldn't convert: invalid literal for int() with base 10: 'five'

else y finally

Una sentencia try puede tener dos partes opcionales más:

  • else se ejecuta solo si el bloque try no lanzó ninguna excepción. Es el lugar para el código que solo debe ejecutarse cuando todo salió bien.
  • finally se ejecuta pase lo que pase: con éxito, con un error manejado o incluso con uno sin manejar. Se usa para tareas de limpieza que siempre deben hacerse.
def load_rate(path):
    try:
        with open(path, encoding="utf-8") as file:
            rate = float(file.read())
    except FileNotFoundError:
        print("Rate file not found. Using the default.")
        rate = 85.0
    except ValueError:
        print("Rate file is damaged. Using the default.")
        rate = 85.0
    else:
        print("Rate loaded from file.")
    finally:
        print("Done checking the rate.")
    return rate

with open("rate.txt", "w", encoding="utf-8") as file:
    file.write("95")

print(load_rate("rate.txt"))
print(load_rate("no_such_file.txt"))
Rate loaded from file.
Done checking the rate.
95.0
Rate file not found. Using the default.
Done checking the rate.
85.0

Como las sentencias with ya se encargan de cerrar los archivos, no necesitarás finally muy seguido, pero conviene reconocerlo.

No Atrapes Todo

Es tentador escribir un except: vacío (o except Exception:) que atrape cualquier error posible. Evítalo: también esconde errores en tu código que querrías conocer, como el nombre de una variable mal escrito.

try:
    total = sum(budgets)       # ups: la variable se llama "budget_list"
except:
    print("Couldn't calculate the total.")

Esto imprime un mensaje vago en lugar del problema real: un NameError que señala directamente el error de escritura. Atrapa solo las excepciones que esperas y sabes manejar, y deja que todo lo demás muestre su traceback mientras desarrollas.

Fallar en Silencio

A veces la respuesta correcta a un error es no hacer nada y seguir adelante. Usa pass, la sentencia de Python que significa “no hacer nada”:

lines = ["1200", "340", "n/a", "210", ""]
visits = []

for line in lines:
    try:
        visits.append(int(line))
    except ValueError:
        pass   # ignora todo lo que no sea un número

print(visits)   # [1200, 340, 210]

Hazlo a propósito, cuando saltarte los datos incorrectos sea realmente lo que quieres, y considera contar o registrar lo que te saltaste, para que los problemas no pasen desapercibidos.

Lanzar tus Propias Excepciones

Las excepciones no son solo para los errores integrados de Python. Tus propias funciones y clases de la Parte 6 y la Parte 7 pueden lanzarlas cuando reciben algo con lo que no pueden trabajar. Usa la palabra clave raise con un tipo de excepción y un mensaje claro:

def calculate_quote(pages, hourly_rate=85):
    if pages < 1:
        raise ValueError("A project needs at least 1 page.")
    return pages * 4 * hourly_rate

try:
    print(calculate_quote(5))   # 1700
    print(calculate_quote(0))
except ValueError as error:
    print(f"Error: {error}")    # Error: A project needs at least 1 page.

Lanzar una excepción es mejor que devolver un valor extraño como -1 o None, porque el problema no se puede ignorar en silencio. El código que llama a tu función decide cómo manejarlo: mostrar un mensaje, volver a preguntar al usuario o dejar que el programa se detenga.

Todo Junto: Una Lista de Clientes que Recuerda

Este es un programa pequeño que guarda una lista de clientes en un archivo JSON. Cada vez que lo ejecutas, carga los clientes que guardaste antes, te deja agregar más y los vuelve a guardar. Maneja un archivo que no existe, un archivo dañado y datos inválidos sin detenerse con un error:

import json
from pathlib import Path

DATA_FILE = Path("clients.json")


def load_clients():
    """Return the saved clients, or an empty list if there aren't any."""
    try:
        with open(DATA_FILE, encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        print(f"Warning: {DATA_FILE} is damaged. Starting with an empty list.")
        return []


def save_clients(clients):
    """Save the clients to the data file."""
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(clients, file, indent=2)


def ask_budget():
    """Ask for a budget until the user enters a valid, positive number."""
    while True:
        answer = input("Budget: $").strip().replace(",", "")
        try:
            budget = float(answer)
        except ValueError:
            print("Please enter a number, like 2500.")
            continue
        if budget <= 0:
            print("The budget must be more than zero.")
            continue
        return budget


clients = load_clients()
print(f"Loaded {len(clients)} saved client(s).")

while True:
    name = input("Client name (or press Enter to finish): ").strip()
    if not name:
        break
    clients.append({"name": name, "budget": ask_budget()})

save_clients(clients)

print("\nAll clients:")
for client in clients:
    print(f"  {client['name']}: ${client['budget']:,.2f}")

La primera ejecución empieza sin archivo:

Loaded 0 saved client(s).
Client name (or press Enter to finish): Bright Bakery
Budget: $2,500
Client name (or press Enter to finish): Valley Dental
Budget: $three hundred
Please enter a number, like 2500.
Budget: $-300
The budget must be more than zero.
Budget: $300
Client name (or press Enter to finish): 

All clients:
  Bright Bakery: $2,500.00
  Valley Dental: $300.00

Ejecútalo de nuevo, y los clientes de la vez anterior siguen ahí:

Loaded 2 saved client(s).
Client name (or press Enter to finish): Calexico Coffee
Budget: $3200
Client name (or press Enter to finish): 

All clients:
  Bright Bakery: $2,500.00
  Valley Dental: $300.00
  Calexico Coffee: $3,200.00

Abre clients.json en tu editor y verás tus datos en JSON simple y legible. Prueba a borrar un corchete para dañarlo y ejecuta el programa otra vez para ver la advertencia.

Errores Comunes de Principiantes

Abrir un archivo en modo "w" cuando querías "a". El modo escritura borra el archivo en el momento en que se abre, aunque nunca escribas nada. Usa "a" para agregar a un archivo, y guarda copias de seguridad de los datos importantes mientras aprendes.

Olvidar cerrar un archivo. Si usas file = open(...) sin with, tienes que llamar tú mismo a file.close(), o es posible que tus datos no se guarden por completo. Usar with lo hace automáticamente.

Equivocarte con la ruta. Un nombre de archivo como "notes.txt" se busca en la carpeta desde la que ejecutaste el programa, que no siempre es la carpeta donde está el script. Si obtienes un FileNotFoundError con un archivo que sí ves, revisa en qué carpeta está tu terminal, o usa la ruta completa.

Olvidar el salto de línea. write() no agrega \n por ti, así que todo termina en una sola línea.

Olvidar que los datos de un archivo son texto. Los números que lees de archivos de texto y CSV son cadenas. Conviértelos con int() o float() antes de hacer operaciones. (JSON conserva los números como números.)

No indicar la codificación. Sin encoding="utf-8", un archivo con letras acentuadas o símbolos puede guardarse o leerse de forma distinta en otra computadora.

Atrapar todas las excepciones. Un except: vacío esconde errores reales. Atrapa las excepciones específicas que esperas.

Poner demasiado código en el bloque try. Limita el bloque try a la línea o las dos líneas que podrían fallar. Si el try abarca 30 líneas, no sabrás cuál lanzó el error, y podrías manejar una excepción que no tenías intención de manejar.

Ejercicios de Práctica

  1. Escribe un programa que pida al usuario su frase favorita y la anexe a quotes.txt. Ejecútalo varias veces y luego escribe un segundo programa que imprima cada frase con un número al inicio.
  2. Escribe una función count_lines(path) que devuelva cuántas líneas tiene un archivo de texto, o 0 si el archivo no existe.
  3. Escribe un programa que lea un archivo de números, uno por línea, que se salte las líneas que no sean números y que imprima el total, el promedio y cuántas líneas se saltó.
  4. Crea un archivo CSV de productos con las columnas name y price. Léelo con csv.DictReader e imprime el producto más caro.
  5. Guarda un diccionario con tus configuraciones en settings.json, y luego escribe un segundo programa que cargue las configuraciones y use un valor predeterminado para cualquier configuración que falte.
  6. Agrega un ValueError a la clase Project de la Parte 7, de modo que crear un proyecto con menos de 1 página lance un error con un mensaje útil.

Conclusión

Los archivos permiten que tus programas recuerden. Con open() y una sentencia with puedes escribir y leer archivos de texto; con los módulos csv y json puedes intercambiar datos con hojas de cálculo y guardar las listas y los diccionarios de tu programa tal como están. pathlib hace que trabajar con rutas de archivos sea sencillo y confiable en cualquier sistema operativo.

Las excepciones permiten que tus programas sobrevivan a lo inesperado. Lee los tracebacks de abajo hacia arriba, envuelve en try el código que podría fallar, maneja en except las excepciones específicas que esperas y usa else y finally cuando los necesites. Cuando tu propio código reciba algo que no puede manejar, lanza una excepción clara con raise en lugar de adivinar.

Junto con todo lo demás de esta serie, ya tienes las herramientas para construir programas completos y prácticos: programas que reciben datos, toman decisiones, organizan su lógica en funciones y clases, conservan sus datos entre ejecuciones y manejan los problemas con elegancia.

Siguiente en la serie Fundamentos de Python: Cómo Probar tu Código, la última parte: cómo demostrar que tu código funciona con pruebas automatizadas.

Serie Fundamentos de Python: Parte 1: Lo que Todo Principiante Debería Saber · Parte 2: Variables y Tipos de Datos · Parte 3: Listas y Diccionarios · Parte 4: La Sentencia If · Parte 5: Entrada del Usuario y Ciclos While · Parte 6: Funciones · Parte 7: Clases · Parte 8: Archivos y Excepciones · Parte 9: Cómo Probar tu Código