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
.pyy ejecútalo conpython3 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 filele 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
| Modo | Nombre | Qué hace |
|---|---|---|
"r" | Lectura | Abre un archivo existente para leerlo. Es el modo predeterminado. Da error si el archivo no existe. |
"w" | Escritura | Crea el archivo, o lo borra si ya existe, y luego escribe. |
"a" | Anexar | Crea el archivo si hace falta y luego agrega al final. |
"x" | Crear | Crea 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=2les 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ón | Cuándo ocurre | Ejemplo |
|---|---|---|
ValueError | Un valor tiene el tipo correcto pero un contenido incorrecto | int("five") |
TypeError | Una operación recibe el tipo equivocado | "Pages: " + 5 |
KeyError | Una clave de diccionario no existe | client["phone"] |
IndexError | Un índice de lista está fuera de rango | pages[10] |
ZeroDivisionError | Dividir entre cero | total / 0 |
FileNotFoundError | Abrir un archivo que no existe | open("missing.txt") |
NameError | Usar 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:
elsese ejecuta solo si el bloquetryno lanzó ninguna excepción. Es el lugar para el código que solo debe ejecutarse cuando todo salió bien.finallyse 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
- 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. - Escribe una función
count_lines(path)que devuelva cuántas líneas tiene un archivo de texto, o0si el archivo no existe. - 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ó.
- Crea un archivo CSV de productos con las columnas
nameyprice. Léelo concsv.DictReadere imprime el producto más caro. - 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. - Agrega un
ValueErrora la claseProjectde 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
