Lectura y escritura de archivos
Leer y escribir con `with open`, qué separa r/w/a/x, el salto de línea al final de cada línea, por qué siempre se escribe `encoding`, y pathlib para las tareas pequeñas.
¿Cómo quieres aprender?
El problema que vamos a resolver
Todo lo que construimos a lo largo de veintidós capítulos desapareció en cuanto el programa terminó.
basket = []
basket.append("pen")
print(basket)['pen']Ejecútalo otra vez y la cesta vuelve a estar vacía. Listas, diccionarios, funciones: todo vive en memoria, y la memoria dura exactamente lo que dura el programa.
El capítulo anterior trataba de compartir código entre programas. Este trata de datos: cómo una ejecución deja su trabajo para la siguiente, o para otra persona.
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("first line\n")
fh.write("second line\n")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(fh.read(), end="")first line
second lineEl programa ha terminado, y notes.txt sigue ahí.
Al terminar este capítulo podrás
- Leer y escribir archivos con
with open(...), y explicar por quéwith - Explicar qué separa
"r","w","a"y"x"— sobre todo qué destruye"w" - Leer un archivo línea por línea, y tratar el
\ndel final de cada una - Explicar por qué siempre se escribe
encoding="utf-8" - Usar
pathlib.Pathpara las tareas pequeñas - Interpretar un
FileNotFoundErrory unUnicodeDecodeError
Requisitos previos: Módulos e importaciones.
Por qué with
Abrir un archivo toma prestado algo del sistema operativo, y todo préstamo hay que devolverlo. Si no cierras el archivo, lo que escribiste puede no llegar nunca al disco, y si hay suficientes archivos sin cerrar, el programa se detiene.
Un bloque with hace esa devolución por ti: el archivo se cierra al terminar el bloque, aunque dentro se haya producido un error.
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(fh.closed)
print(fh.read())True
ValueError: I/O operation on closed file.(Antes de la línea del error, Python también imprime unas líneas de rastreo; en este capítulo solo se muestra la última.)
El nombre fh sigue existiendo después del bloque — el ámbito funciona igual que antes — pero el archivo está cerrado. En este curso, open() siempre se escribe con with.
Leer
Todo el contenido de una vez se obtiene con .read():
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(repr(text))
print(len(text))'first line\nsecond line\n'
23repr se usa a propósito, porque un print normal oculta los saltos de línea. Un archivo es un único texto largo, y sus saltos de línea están dentro de él como \n — recordar eso es la mitad de este capítulo.
Una línea cada vez
.read() no es buena opción para un archivo grande, porque carga todo el contenido en memoria. Recorrer el propio archivo hace que Python lea una línea cada vez:
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'Cada línea termina en \n. Esta es la confusión más común de todas — line == "pen" no coincide, "pen\n" y "pen" son claves de diccionario distintas, y el motivo no se ve. (int() y float() ignoran por sí solos los espacios y el \n alrededor del número, así que int("3\n") sí da 3 — pero una cadena se guarda exactamente como está, así que el \n se mantiene en las comparaciones y en la salida impresa.)
Por eso el primer paso casi siempre es .strip():
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
name, price, qty = line.strip().split(",")
print(f"{name:<5} {float(price) * int(qty):>8.2f}")pen 45.00
bag 850.00
ink 240.00.strip(), luego .split(","), luego float() e int() — esa secuencia volverá una y otra vez al leer archivos. Todo lo que sale de un archivo es texto, nunca números; la conversión que vimos en el capítulo siete también hace falta aquí.
Cuando se quieren todas las líneas a la vez, hay dos formas:
with open("orders.txt", "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
print(lines)['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2'].splitlines() quita los saltos de línea. También existe .readlines(), pero conserva el \n — y ahí es donde tropieza la mayoría.
Escribir, y una letra peligrosa
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("new\n")El archivo contenía antes old content. Ahora:
new"w" vacía el archivo — en el momento de abrirlo, antes de escribir nada. No hay aviso ni marcha atrás.
Para conservar lo que hay y añadir al final, "a":
with open("notes.txt", "a", encoding="utf-8") as fh:
fh.write("new\n")old content
newY para negarse a escribir si el archivo ya existe, "x":
FileExistsError: [Errno 17] File exists: 'notes.txt'Los cuatro modos en una línea: "r" lee, "w" borra y escribe, "a" añade al final, "x" solo crea.
Haz una pausa antes de escribir"w". La gente pierde trabajo real por esta letra. Si el archivo que vas a abrir no lo creó tu programa, piénsalo antes de escribir"w".
El \n lo pones tú
print hace el salto de línea por ti. write no.
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)'penbag'Así que la forma más limpia de escribir una lista de líneas es:
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
fh.write("\n".join(rows) + "\n")'pen\nbag\n'"\n".join(rows) pone saltos entre las líneas, y el + "\n" final termina el archivo con un salto de línea — que es la norma habitual en los archivos de texto y lo que esperan muchas herramientas.
encoding="utf-8" — siempre
Sin encoding, Python usa la codificación por defecto del sistema operativo, que cambia de una máquina a otra. El resultado es un programa que funciona en tu ordenador y se rompe en el de otra persona.
Leer con una codificación equivocada da:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byteAsí que escribe encoding="utf-8" cada vez que uses open() — tanto para leer como para escribir. La única excepción es el modo binario ("rb", "wb"), donde la cuestión no surge porque no hay codificación.
pathlib — una forma corta para tareas pequeñas
Para un archivo pequeño, todo el bloque with puede parecer mucho.
from pathlib import Path
path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))
out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")True
3
doneread_text y write_text abren y cierran el archivo internamente, así que no hace falta with. Path hace más cosas — .exists(), .name, .suffix, .parent — y las rutas de carpetas se unen con /, como en Path("data") / "orders.txt".
¿Cuándo usar cada uno? Path cuando se quiere el archivo entero de una vez, with open(...) cuando se lee línea a línea. Ten en cuenta una cosa: write_text siempre se comporta como "w" — si el archivo ya existe, su contenido anterior se borra sin avisar. No tiene forma de añadir; para eso necesitas with open(..., "a").
Un ejemplo completo
orders.txt:
pen,15.0,3
bag,850.0,1
ink,120.0,2main.py:
"""Read an order file, total it, and write a report beside it."""
from pathlib import Path
TAX_RATE = 0.15
def read_order(path):
"""Each line is name,price,quantity. Blank lines are skipped."""
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
name, price, quantity = raw.split(",")
lines.append((name, float(price), int(quantity)))
return lines
def report(lines):
rows = []
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
rows.append("-" * 20)
rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
return rows
def main():
order = read_order(Path("orders.txt"))
rows = report(order)
Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"), end="")
print()
print("lines read:", len(order))
if __name__ == "__main__":
main()pen 3 51.75
bag 1 977.50
ink 2 276.00
--------------------
total 1305.25
lines read: 3Cuatro cosas que vale la pena mirar.
Tres funciones con tres trabajos separados. read_order lee del disco, report solo calcula, y main escribe. A report no se le da ningún archivo — recibe una lista y devuelve una lista. Así que probarla no necesita archivo, y en el capítulo veintiocho eso será justo lo que ayude.
Las líneas en blanco se tratan de antemano. Un único \n al final del archivo no produce una línea en blanco — .splitlines() lo descarta. Pero en un archivo editado a mano, una línea en blanco de verdad (dos \n seguidos) en medio o al final es algo de lo más normal, y sin tratarla, raw.split(",") no daría tres valores y lanzaría un ValueError. if not raw.strip(): continue lo resuelve en una línea.
float(price) e int(quantity) no son opcionales, porque todo lo que sale de un archivo llega como texto. Si los olvidas, price * quantity repetiría la cadena en silencio — el peor tipo de error, el que no rompe nada.
El informe va a un archivo aparte. orders.txt nunca se abre con "w" — el archivo de entrada se queda intacto. Es un hábito que conviene adquirir: no escribas en lo que estás leyendo.
Cuando falla
FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' El archivo no está ahí, o estás ejecutando el programa desde otra carpeta. La ruta se calcula a partir de dónde lo ejecutaste, no de la carpeta del script. Imprime Path("orders.txt").resolve() para ver dónde está buscando.
UnicodeDecodeError: 'utf-8' codec can't decode byte ... El archivo se escribió con otra codificación, o ni siquiera es un archivo de texto. Corrige el encoding, o ábrelo con "rb" si es binario.
ValueError: I/O operation on closed file. El archivo se usó fuera de su bloque with. Lee lo que necesites dentro del bloque.
ValueError: not enough values to unpack Alguna línea no tiene las comas que esperabas — a menudo es una línea en blanco. Salta las líneas en blanco, y si tienes dudas, imprime print(repr(raw)).
Escribí el archivo, pero está vacío Se está leyendo antes de que termine el bloque with. Lo escrito llega al disco cuando el archivo se cierra.
Todas mis líneas se juntaron en una sola write no hace saltos de línea. El "\n" lo pones tú.
Todo el contenido de mi archivo ha desaparecido Se abrió con "w". Usa "r" para leer y "a" para añadir al final.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Se usa repr, así que nada queda oculto. ¿Qué se imprime?
# orders.txt
# pen,15.0,3
# bag,850.0,1
# ink,120.0,2
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))- A'pen,15.0,3' 'bag,850.0,1' 'ink,120.0,2'
- B'pen,15.0,3\nbag,850.0,1\nink,120.0,2\n'
- C'pen,15.0,3\n' 'bag,850.0,1\n' 'ink,120.0,2\n'
- D['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']
El archivo se abre con "w", pero no se escribe nada. ¿Qué se imprime?
# notes.txt already contains: important work
with open("notes.txt", "w", encoding="utf-8") as fh:
print("opened")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- Aopened 'opened\n'
- BUn `FileExistsError`
- Copened 'important work\n'
- Dopened ''
Se escriben dos cosas por separado. ¿Qué hay en el archivo?
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)
with open("out.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- A'pen\nbag\n'
- B'pen\nbag'
- C'pen bag'
- D'penbag'
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Crea un archivo llamado people.txt con un nombre y un número en cada línea, separados por una coma — al menos cuatro líneas, y deja a propósito una línea en blanco en medio.
Después escribe summary.py con tres funciones: una que lea el archivo y devuelva una lista de pares, otra que convierta esa lista en líneas de texto (quién tiene el número más grande, y el total), y main, que lo escriba en summary.txt. No olvides if __name__ == "__main__":.
Luego, cinco experimentos:
- Quita el
.strip(), luego imprime la última parte de cada línea conrepr(). ¿int()da error? (No —int("10\n")da10.) Entonces, ¿dónde se esconde el\n, y qué pasa con la línea en blanco? - Borra un número de una línea, de modo que la coma esté pero el número no. Lee el mensaje.
- Cambia
"w"por"a"enmain, luego ejecuta el programa tres veces. ¿Qué le pasó asummary.txt? - Intenta abrir
people.txtcon"w"... no lo hagas. Haz una copia y pruébalo con ella, luego mira qué queda. - Imprime
Path("people.txt").resolve(), luego ejecuta el programa desde otra carpeta. ¿Cambia la ruta?
Esta última explica un error que casi todo el mundo comete una vez: el programa funcionó, luego se ejecutó desde otro sitio y lanzó un FileNotFoundError.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz