Archivos de texto en Python — práctica con el resumen del diario, sesiones que se apuntan sin borrar nada y un informe del mes (y la sesión que se pegó a la del día anterior)
Después de perder el diario con un 'w' (te lo conté en la teoría), me pasé al modo 'a' y me sentí a salvo. Dos semanas después, corregí a mano una sesión en el Bloc de notas, guardé y volví a apuntar la del día con mi programa. Al hacer el resumen del mes me salió un aviso rarísimo: minutos no válidos en la línea de la bici. Abrí el archivo y me encontré esto: 2026-10-14;bici;452026-10-15;carrera;40. Al editar a mano se me había ido el salto de línea del final, y 'a' escribió la sesión nueva pegada a la anterior. La lección: 'a' no se lleva lo que hay, pero tampoco lo arregla. Escribe justo detrás del último carácter, sea cual sea.
En esta práctica, que sigue a la teoría de archivos de texto, hacemos tres programas de nivel medio con el mismo diario de entrenamientos: un resumen que lee el archivo y sobrevive a las líneas raras, un registro que añade sesiones sin borrar ni pegar nada, y un informe que se escribe en un archivo nuevo, en su propia carpeta. Los datos se parecen a los de verdad a propósito: comentarios, líneas vacías, espacios de más, una sesión con los minutos en letras y otra a la que le falta un campo.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Los modos
'r','w'y'a', y qué hace cada uno con lo que ya hay en el archivo. - Que cada línea leída trae su
'\n'y questrip()lo quita (junto con espacios y tabuladores de los extremos). - Recorrer un archivo con
for linea in fy escribir conprint(..., file=f)ywrite(). - Diccionarios con
get()para contar y acumular.
Si alguno te suena dudoso, repasa la teoría antes de seguir.
Guarda cada programa en una carpeta junto a su archivo de datos y ejecútalo desde esa carpeta. Si la carpeta de trabajo es otra, open('entrenos.txt') dará FileNotFoundError. Y abre siempre con encoding='utf-8': el diario tiene natación y fútbol.
El archivo entrenos.txt de los programas 1 y 3 es este (la sexta línea lleva además dos espacios y un tabulador al final, invisibles aquí):
# diario de entrenamientos: fecha;deporte;minutos 2026-10-05;carrera;35 2026-10-06;bici;50 2026-10-08;natación;40 2026-10-09; carrera ;30 2026-10-11;yoga;veinte 2026-10-12;fútbol;90 2026-10-13;carrera 2026-10-14;bici;45
Programa 1: el resumen del diario
Queremos leer entrenos.txt y sacar cuántas sesiones válidas hay, los minutos por deporte, el total y el «deporte estrella» (el de más minutos). Lo que lo complica: el archivo tiene un comentario, una línea vacía, espacios de más, una sesión con veinte y otra sin minutos. El programa no puede romperse por ninguna de ellas, y además tiene que decir en qué línea está cada problema.
La idea
Separamos el trabajo en tres funciones, cada una con una sola tarea:
leer_sesiones()recorre el archivo conforyenumerate(), limpia cada línea y decide: o es una sesión válida (va a una lista de tuplas), o se salta (vacía o comentario), o deja un aviso con su número de línea.minutos_por_deporte()convierte la lista de sesiones en un diccionariodeporte -> minutos.deporte_estrella()busca el máximo a mano.
Leer y calcular van separados: así, si mañana el diario viene de otro sitio, solo cambia la primera función.
Antes de programar, piensa en qué puede ir mal en cada línea y en qué orden comprobarlo: primero si está vacía, después si tiene tres campos y por último si los minutos son un número. Si lo haces al revés, el código revienta antes de llegar a la comprobación.
El código
# Programa 1 · El resumen del diario
def leer_sesiones(nombre):
"""Devuelve las sesiones válidas (fecha, deporte, minutos) y los avisos de las líneas raras."""
sesiones = []
avisos = []
with open(nombre, 'r', encoding='utf-8') as f:
for numero, linea in enumerate(f, start=1): # el número sirve para decir DÓNDE está el fallo
linea = linea.strip() # fuera '\n' y espacios de los extremos
if linea == '' or linea.startswith('#'):
continue # vacías y comentarios no son sesiones
partes = linea.split(';')
if len(partes) != 3: # sin esto, desempaquetar daría ValueError
avisos.append(f'línea {numero}: faltan campos')
continue
fecha, deporte, minutos = [p.strip() for p in partes] # ' carrera ' -> 'carrera'
if not minutos.isdigit(): # int('veinte') daría ValueError
avisos.append(f'línea {numero}: minutos no válidos ({minutos})')
continue
sesiones.append((fecha, deporte, int(minutos)))
return sesiones, avisos
def minutos_por_deporte(sesiones):
"""Diccionario deporte -> minutos totales."""
totales = {}
for fecha, deporte, minutos in sesiones:
totales[deporte] = totales.get(deporte, 0) + minutos # get: 0 la primera vez que sale
return totales
def deporte_estrella(totales):
"""El deporte con más minutos (si empatan, el primero en orden alfabético)."""
mejor = None
for deporte in sorted(totales):
if mejor is None or totales[deporte] > totales[mejor]: # > y no >=: el empate no cambia el mejor
mejor = deporte
return mejor
sesiones, avisos = leer_sesiones('entrenos.txt')
print('Sesiones válidas:', len(sesiones))
for aviso in avisos:
print(' Aviso,', aviso)
totales = minutos_por_deporte(sesiones)
for deporte in sorted(totales): # orden alfabético (el inglés conserva el mismo)
print(f'{deporte:<10}{totales[deporte]:>4} min')
print('Total:', sum(totales.values()), 'min')
estrella = deporte_estrella(totales)
print('Deporte estrella:', estrella, f'({totales[estrella]} min)')
Lo más delicado es leer_sesiones(). Vamos a verla de cerca:
def leer_sesiones(nombre):
"""Devuelve las sesiones válidas (fecha, deporte, minutos) y los avisos de las líneas raras."""
sesiones = []
avisos = []
with open(nombre, 'r', encoding='utf-8') as f:
for numero, linea in enumerate(f, start=1): # el número sirve para decir DÓNDE está el fallo
linea = linea.strip() # fuera '\n' y espacios de los extremos
if linea == '' or linea.startswith('#'):
continue # vacías y comentarios no son sesiones
partes = linea.split(';')
if len(partes) != 3: # sin esto, desempaquetar daría ValueError
avisos.append(f'línea {numero}: faltan campos')
continue
fecha, deporte, minutos = [p.strip() for p in partes] # ' carrera ' -> 'carrera'
if not minutos.isdigit(): # int('veinte') daría ValueError
avisos.append(f'línea {numero}: minutos no válidos ({minutos})')
continue
sesiones.append((fecha, deporte, int(minutos)))
return sesiones, avisos
enumerate(f, start=1)numera las líneas desde 1, como cualquier editor. Ese número es el que sale en los avisos, para que encuentres el fallo a la primera.linea.strip()quita el'\n', los espacios y el tabulador de la línea 6. Tras elstrip(), la línea vacía sí es'', y elcontinuela salta junto al comentario.len(partes) != 3se comprueba antes de desempaquetar.fecha, deporte, minutos = ['2026-10-13', 'carrera']daríaValueError: not enough values to unpack.[p.strip() for p in partes]limpia cada campo:strip()sobre la línea no toca los espacios de' carrera ', que están en medio.minutos.isdigit()evita elValueErrordeint('veinte').isdigit()esTruesolo si todos los caracteres son dígitos.
Esta es la traza de las diez líneas:
| Línea | Tras strip() | Decisión | sesiones / avisos |
|---|---|---|---|
| 1 | '# diario de entrenamientos: …' | empieza por #: se salta | — |
| 2 | '2026-10-05;carrera;35' | válida | + ('2026-10-05', 'carrera', 35) |
| 3 | '2026-10-06;bici;50' | válida | + ('2026-10-06', 'bici', 50) |
| 4 | '' | vacía: se salta | — |
| 5 | '2026-10-08;natación;40' | válida | + (…, 'natación', 40) |
| 6 | '2026-10-09; carrera ;30' | válida (campos limpios) | + (…, 'carrera', 30) |
| 7 | '2026-10-11;yoga;veinte' | isdigit() falso | aviso de la línea 7 |
| 8 | '2026-10-12;fútbol;90' | válida | + (…, 'fútbol', 90) |
| 9 | '2026-10-13;carrera' | solo 2 campos | aviso de la línea 9 |
| 10 | '2026-10-14;bici;45' | válida | + (…, 'bici', 45) |
El dibujo resume la tabla y enseña adónde va a parar cada línea:
Las otras dos funciones son cortas. minutos_por_deporte() es el patrón de acumular con get():
def minutos_por_deporte(sesiones):
"""Diccionario deporte -> minutos totales."""
totales = {}
for fecha, deporte, minutos in sesiones:
totales[deporte] = totales.get(deporte, 0) + minutos # get: 0 la primera vez que sale
return totales
La primera vez que aparece 'bici', totales.get('bici', 0) devuelve 0 y queda 0 + 50 = 50; la segunda, 50 + 45 = 95. Y deporte_estrella() busca el máximo recorriendo las claves en orden alfabético:
def deporte_estrella(totales):
"""El deporte con más minutos (si empatan, el primero en orden alfabético)."""
mejor = None
for deporte in sorted(totales):
if mejor is None or totales[deporte] > totales[mejor]: # > y no >=: el empate no cambia el mejor
mejor = deporte
return mejor
El > (y no >=) hace que, si dos deportes empatan, gane el primero en orden alfabético: el segundo no es mayor, solo igual, y no cambia mejor.
Salida
Esto es lo que sale con el diario de arriba:
Sesiones válidas: 6 Aviso, línea 7: minutos no válidos (veinte) Aviso, línea 9: faltan campos bici 95 min carrera 65 min fútbol 90 min natación 40 min Total: 290 min Deporte estrella: bici (95 min)
Comprobémoslo: bici, 50 + 45 = 95; carrera, 35 + 30 = 65 (la de la línea 9 no cuenta); fútbol, 90; natación, 40. Total: 95 + 65 + 90 + 40 = 290, y 6 sesiones válidas (las líneas 2, 3, 5, 6, 8 y 10). El yoga no aparece porque su única sesión era la de veinte. Y la bici gana con 95 frente a los 90 del fútbol.
f'{deporte:<10}{totales[deporte]:>4} min' alinea las columnas: el nombre a la izquierda en 10 caracteres y los minutos a la derecha en 4. Lo viste en el tema de Strings.
Preguntas que te puedes hacer
**¿Por qué no uso try/except para int('veinte')?** Podrías, y también estaría bien. Con isdigit() la comprobación se lee en una línea y no esconde otros errores sin querer. Si te sientes cómodo con las excepciones en Python, try: int(minutos) except ValueError: es igual de válido.
¿Por qué guardo tuplas y no escribo directamente en el diccionario? Porque así leer_sesiones() solo lee, y cualquier otro cálculo (sesiones por semana, la más larga…) se hace sobre la misma lista sin volver a abrir el archivo.
**¿Por qué with y no open/close?** Porque return sale de la función dentro del bloque y with cierra el archivo igualmente. Con close() tendrías que acordarte de ponerlo antes de cada salida.
**¿Por qué sorted(totales) para imprimir?** Para que el resumen salga siempre en el mismo orden (alfabético), sea cual sea el orden en que aparecen los deportes en el archivo.
¿Y si el archivo no existe? open() lanza FileNotFoundError en la primera línea de leer_sesiones(). Puedes comprobarlo antes con os.path.exists('entrenos.txt'), como en el Programa 3.
Desempaquetar sin comprobar el número de campos. fecha, deporte, minutos = linea.split(';') funciona con las líneas buenas y revienta en la primera que tenga un campo de menos (la línea 9). Comprueba siempre len(partes) antes.
Pruébalo tú
- Añade un aviso para las líneas con un deporte que no conoces (por ejemplo,
pesas) usando un conjuntoDEPORTES. - Haz que el resumen diga también la sesión más larga y su fecha.
Programa 2: apuntar sesiones sin perder nada
Ahora el diario se va ampliando. El programa recibe sesiones nuevas y las añade al final de diario.txt. Lo que lo complica: hay que validar antes de escribir (un deporte que no conocemos o 0 minutos no se apuntan) y el archivo puede venir sin salto de línea al final, como me pasó a mí. Este es el diario.txt de partida; la última línea no tiene '\n':
2026-10-12;fútbol;90 2026-10-14;bici;45
La idea
El modo es 'a', nunca 'w'. Pero 'a' escribe justo detrás del último carácter, así que antes de escribir miramos si el archivo termina en '\n'. Si no, escribimos primero el salto que falta. Tres funciones:
termina_en_salto()lee el archivo (es pequeño) y comprueba el último carácter.registrar()valida, añade el salto si hace falta y escribe la sesión con su'\n'.mostrar_numerado()enseña el diario con números de línea, para ver el antes y el después.
Antes de programar, piensa en el estado en el que puede estar el archivo: vacío, terminado en '\n' o terminado sin él. Tu código tiene que dejar las tres situaciones bien.
El código
# Programa 2 · Apuntar sesiones sin perder nada
DEPORTES = {'bici', 'carrera', 'fútbol', 'natación', 'yoga'}
def termina_en_salto(nombre):
"""True si el archivo está vacío o acaba en '\n' (se puede añadir sin pegar líneas)."""
with open(nombre, 'r', encoding='utf-8') as f:
texto = f.read() # el diario es pequeño: leerlo entero no cuesta nada
return texto == '' or texto.endswith('\n')
def registrar(nombre, fecha, deporte, minutos):
"""Añade una sesión al final del diario. Devuelve True si la ha escrito."""
if deporte not in DEPORTES or minutos <= 0: # mejor no escribir que escribir basura
print(' No apunto', fecha, deporte, minutos, '(datos raros)')
return False
salto = '' if termina_en_salto(nombre) else '\n' # cierra antes la última línea si le falta su '\n'
with open(nombre, 'a', encoding='utf-8') as f: # 'a' y nunca 'w', que vaciaría el diario
f.write(f'{salto}{fecha};{deporte};{minutos}\n')
return True
def mostrar_numerado(nombre):
with open(nombre, 'r', encoding='utf-8') as f:
for numero, linea in enumerate(f, start=1):
texto = linea.rstrip('\n') # quito solo el salto, no los espacios
print(f'{numero:>3} | {texto}')
print('Antes:')
mostrar_numerado('diario.txt')
apuntadas = 0
for sesion in [('2026-10-15', 'carrera', 40), ('2026-10-16', 'pesas', 30),
('2026-10-17', 'natación', 0), ('2026-10-18', 'yoga', 25)]:
if registrar('diario.txt', *sesion): # *sesion reparte la tupla en fecha, deporte y minutos
apuntadas += 1
print('Apuntadas:', apuntadas)
print('Después:')
mostrar_numerado('diario.txt')
La función clave es termina_en_salto():
def termina_en_salto(nombre):
"""True si el archivo está vacío o acaba en '\n' (se puede añadir sin pegar líneas)."""
with open(nombre, 'r', encoding='utf-8') as f:
texto = f.read() # el diario es pequeño: leerlo entero no cuesta nada
return texto == '' or texto.endswith('\n')
texto.endswith('\n') mira el último carácter. El caso texto == '' va aparte porque un archivo vacío no termina en salto, pero tampoco hace falta añadir uno: la primera sesión irá en la primera línea. El orden del or importa poco aquí, pero ponerlo primero deja claro que es un caso especial.
Y registrar(), que decide qué se escribe:
def registrar(nombre, fecha, deporte, minutos):
"""Añade una sesión al final del diario. Devuelve True si la ha escrito."""
if deporte not in DEPORTES or minutos <= 0: # mejor no escribir que escribir basura
print(' No apunto', fecha, deporte, minutos, '(datos raros)')
return False
salto = '' if termina_en_salto(nombre) else '\n' # cierra antes la última línea si le falta su '\n'
with open(nombre, 'a', encoding='utf-8') as f: # 'a' y nunca 'w', que vaciaría el diario
f.write(f'{salto}{fecha};{deporte};{minutos}\n')
return True
- Si el deporte no está en
DEPORTESo los minutos no son positivos, no se escribe nada y se devuelveFalse. Mejor no apuntar que apuntar basura que luego rompe el resumen. salto = '' if termina_en_salto(nombre) else '\n'es unifen una línea:saltovale''si todo está bien, o'\n'si a la última línea le falta su salto.f.write(f'{salto}{fecha};{deporte};{minutos}\n'): la línea nueva siempre acaba en'\n', así que la siguiente vez el archivo ya estará bien.
La traza de las cuatro sesiones del bucle:
| Vuelta | Sesión | ¿Válida? | ¿Acaba en '\n'? | Se escribe |
|---|---|---|---|---|
| 1 | ('2026-10-15', 'carrera', 40) | sí | no | '\n2026-10-15;carrera;40\n' |
| 2 | ('2026-10-16', 'pesas', 30) | no: pesas | — | nada |
| 3 | ('2026-10-17', 'natación', 0) | no: 0 minutos | — | nada |
| 4 | ('2026-10-18', 'yoga', 25) | sí | sí | '2026-10-18;yoga;25\n' |
El dibujo enseña lo que habría pasado sin la comprobación de la vuelta 1:
Salida
Al ejecutarlo, con el diario sin salto final:
Antes: 1 | 2026-10-12;fútbol;90 2 | 2026-10-14;bici;45 No apunto 2026-10-16 pesas 30 (datos raros) No apunto 2026-10-17 natación 0 (datos raros) Apuntadas: 2 Después: 1 | 2026-10-12;fútbol;90 2 | 2026-10-14;bici;45 3 | 2026-10-15;carrera;40 4 | 2026-10-18;yoga;25
Antes había 2 líneas; se intentan 4 sesiones, se rechazan 2 (pesas y los 0 minutos) y se apuntan 2: 2 + 2 = 4 líneas después. La línea 2 conserva su 45 y la sesión de carrera empieza en su propia línea 3: el salto que faltaba se escribió antes que ella.
Preguntas que te puedes hacer
¿Por qué leo el archivo entero para mirar un solo carácter? Porque el diario es pequeño y read() es lo más sencillo que has visto. Con archivos enormes se haría de otra forma, pero aquí no merece la pena.
**¿Qué hace *sesion?** Reparte la tupla en argumentos: registrar('diario.txt', *('2026-10-15', 'carrera', 40)) es lo mismo que registrar('diario.txt', '2026-10-15', 'carrera', 40).
**¿Por qué linea.rstrip('\n') en mostrar_numerado() y no strip()?** Para quitar solo el salto. Si hubiera espacios importantes al principio o al final, se verían tal cual.
**¿Por qué DEPORTES es un conjunto?** Porque solo preguntamos «¿está dentro?» y para eso un conjunto es lo natural. El orden no importa.
¿Y si dos programas apuntan a la vez? En esta práctica no pasa. En la vida real habría que tener cuidado, pero eso ya es tema de sistemas operativos.
Usar 'w' en registrar(). Cada sesión nueva vaciaría el diario y solo quedaría la última. Y el error contrario, más sutil: usar 'a' y dar por hecho que el archivo acaba en '\n'.
Pruébalo tú
- Haz que
registrar()rechace también las fechas que ya están en el diario (lee las fechas en un conjunto). - Añade una función
deshacer()que quite la última sesión: lee todas las líneas, abre en'w'y escribe todas menos la última.
Programa 3: el informe del mes en un archivo nuevo
El último programa lee entrenos.txt y escribe un informe con una tabla (deporte, sesiones y minutos), una fila de totales y la media por sesión. El informe va en un archivo nuevo, informes/octubre.txt, dentro de una carpeta que puede no existir. Aquí el 'w' sí es lo correcto: el informe se rehace entero cada vez.
La idea
Es una tubería de tres funciones: leer → resumir → escribir. Los datos pasan de archivo a lista, de lista a diccionario y de diccionario a archivo:
Para la carpeta usamos os.makedirs(..., exist_ok=True) y para la ruta os.path.join(), así funciona igual en Windows y en Linux. Al final volvemos a leer el informe para comprobar lo que ha quedado escrito.
Antes de programar, dibuja en papel cómo quieres que quede el informe, con los anchos de cada columna. Luego copia esos anchos en las f-strings de la cabecera y de las filas: tienen que ser los mismos.
El código
# Programa 3 · El informe del mes en un archivo nuevo
import os
def leer_validas(nombre):
"""Lista de (deporte, minutos) de las líneas correctas; las demás se saltan."""
validas = []
with open(nombre, 'r', encoding='utf-8') as f:
for linea in f:
partes = [p.strip() for p in linea.split(';')]
if len(partes) == 3 and partes[2].isdigit(): # comentarios y vacías no tienen 3 campos
validas.append((partes[1], int(partes[2])))
return validas
def resumen(validas):
"""Diccionario deporte -> [sesiones, minutos]."""
datos = {}
for deporte, minutos in validas:
if deporte not in datos:
datos[deporte] = [0, 0]
datos[deporte][0] += 1
datos[deporte][1] += minutos
return datos
def escribir_informe(datos, ruta):
with open(ruta, 'w', encoding='utf-8') as f: # 'w' a propósito: el informe se rehace entero
print('INFORME DE ENTRENAMIENTOS · OCTUBRE', file=f)
print('=' * 28, file=f)
print(f'{"Deporte":<10}{"Sesiones":>9}{"Minutos":>9}', file=f)
total_s = total_m = 0
for deporte in sorted(datos):
s, m = datos[deporte]
print(f'{deporte:<10}{s:>9}{m:>9}', file=f) # mismos anchos que la cabecera
total_s += s
total_m += m
print('-' * 28, file=f)
print(f'{"Total":<10}{total_s:>9}{total_m:>9}', file=f)
f.write(f'Media por sesión: {total_m / total_s:.1f} min\n') # write: el '\n' lo pongo yo
validas = leer_validas('entrenos.txt')
os.makedirs('informes', exist_ok=True)
ruta = os.path.join('informes', 'octubre.txt')
escribir_informe(resumen(validas), ruta)
print('Escrito:', ruta, '¿existe?', os.path.exists(ruta))
with open(ruta, 'r', encoding='utf-8') as f: # lo leo para comprobar lo que ha quedado
print(f.read(), end='')
leer_validas() es una versión más corta del Programa 1, que se salta sin avisar lo que no le sirve:
def leer_validas(nombre):
"""Lista de (deporte, minutos) de las líneas correctas; las demás se saltan."""
validas = []
with open(nombre, 'r', encoding='utf-8') as f:
for linea in f:
partes = [p.strip() for p in linea.split(';')]
if len(partes) == 3 and partes[2].isdigit(): # comentarios y vacías no tienen 3 campos
validas.append((partes[1], int(partes[2])))
return validas
El truco está en que el comentario, la línea vacía y la línea 9 no tienen 3 campos, y 'veinte'.isdigit() es falso: una sola condición filtra las cuatro líneas raras.
resumen() crea un diccionario de listas [sesiones, minutos]:
def resumen(validas):
"""Diccionario deporte -> [sesiones, minutos]."""
datos = {}
for deporte, minutos in validas:
if deporte not in datos:
datos[deporte] = [0, 0]
datos[deporte][0] += 1
datos[deporte][1] += minutos
return datos
| Sesión | datos después |
|---|---|
('carrera', 35) | {'carrera': [1, 35]} |
('bici', 50) | {'carrera': [1, 35], 'bici': [1, 50]} |
('natación', 40) | … 'natación': [1, 40] |
('carrera', 30) | 'carrera': [2, 65] |
('fútbol', 90) | … 'fútbol': [1, 90] |
('bici', 45) | 'bici': [2, 95] |
Y escribir_informe() escribe la tabla:
def escribir_informe(datos, ruta):
with open(ruta, 'w', encoding='utf-8') as f: # 'w' a propósito: el informe se rehace entero
print('INFORME DE ENTRENAMIENTOS · OCTUBRE', file=f)
print('=' * 28, file=f)
print(f'{"Deporte":<10}{"Sesiones":>9}{"Minutos":>9}', file=f)
total_s = total_m = 0
for deporte in sorted(datos):
s, m = datos[deporte]
print(f'{deporte:<10}{s:>9}{m:>9}', file=f) # mismos anchos que la cabecera
total_s += s
total_m += m
print('-' * 28, file=f)
print(f'{"Total":<10}{total_s:>9}{total_m:>9}', file=f)
f.write(f'Media por sesión: {total_m / total_s:.1f} min\n') # write: el '\n' lo pongo yo
print(..., file=f)en casi todo: añade el salto por nosotros.- La última línea va con
f.write()para que veas las dos formas juntas: ahí el'\n'lo pongo yo. f'{"Deporte":<10}{"Sesiones":>9}{"Minutos":>9}'yf'{deporte:<10}{s:>9}{m:>9}'usan los mismos anchos (10, 9 y 9), y por eso las columnas cuadran.total_s = total_m = 0pone las dos variables a 0 en una línea.
Salida
La consola enseña la ruta y, después, el informe leído del disco:
Escrito: informes/octubre.txt ¿existe? True INFORME DE ENTRENAMIENTOS · OCTUBRE ============================ Deporte Sesiones Minutos bici 2 95 carrera 2 65 fútbol 1 90 natación 1 40 ---------------------------- Total 6 290 Media por sesión: 48.3 min
Comprobación: 2 + 2 + 1 + 1 = 6 sesiones y 95 + 65 + 90 + 40 = 290 minutos, los mismos números que en el Programa 1. La media es 290 / 6 = 48,33…, que con :.1f se redondea a 48.3. Cada fila mide 10 + 9 + 9 = 28 caracteres, igual que las líneas de = y -.
Si ejecutas el programa dos veces, el informe no se duplica: 'w' lo vacía y lo escribe de nuevo. Es justo lo que queremos aquí, y justo lo que no queríamos en el diario.
Preguntas que te puedes hacer
**¿Por qué os.path.join() y no 'informes/octubre.txt'?** Las dos funcionan en Windows con Python, pero join() usa el separador del sistema y te evita líos al montar rutas a partir de variables.
¿Por qué vuelvo a leer el archivo al final? Para comprobar que se escribió lo que creía. Es una buena costumbre con archivos: lo que importa es lo que queda en el disco.
**¿Por qué print(f.read(), end='')?** El informe ya acaba en '\n'; sin end='' saldría una línea en blanco al final.
**¿Qué pasa si entrenos.txt no tiene ninguna sesión válida?** total_s sería 0 y la media daría ZeroDivisionError. Es la primera variación de abajo.
Anchos distintos en la cabecera y en las filas. Si la cabecera usa :<10 y las filas :<8, la tabla se tuerce y no sabrás por qué. Copia los mismos números.
Pruébalo tú
- Protege la media: si no hay sesiones, escribe
Sin sesiones este mesen lugar de dividir. - Ordena las filas por minutos, de más a menos, en lugar de por orden alfabético.
Errores típicos en esta práctica
- **Abrir el diario con
'w':** vacía el archivo; para ampliarlo, siempre'a'. - **Dar por hecho que el archivo acaba en
'\n':** si lo editaste a mano, puede no hacerlo y la sesión nueva se pega. - Desempaquetar sin contar los campos: la primera línea incompleta rompe el programa con
ValueError. - **Convertir con
int()sin comprobar:**int('veinte')también daValueError. - Limpiar la línea pero no los campos:
' carrera 'y'carrera'serían dos deportes distintos en el diccionario. - Cabecera y filas con anchos distintos: la tabla del informe sale torcida.
Míralo paso a paso en Python Tutor
En Python Tutor (elige Python 3) no se pueden abrir archivos del disco, así que el «archivo» es una lista de líneas, cada una con su '\n'. El bucle es el mismo que con for linea in f.
# El Programa 1 en pequeño, con el "archivo" simulado por una lista de líneas
archivo = ['# fecha;deporte;minutos\n', '2026-10-05;carrera;35\n', '\n',
'2026-10-06;bici;50\n', '2026-10-09; carrera ;30\n']
totales = {}
for linea in archivo: # con un archivo de verdad sería: for linea in f
linea = linea.strip()
if linea == '' or linea.startswith('#'):
continue
fecha, deporte, minutos = [p.strip() for p in linea.split(';')]
totales[deporte] = totales.get(deporte, 0) + int(minutos)
print(totales)
Al final verás:
{'carrera': 65, 'bici': 50}
Qué observar:
- En la primera vuelta,
lineaes el comentario: tras elstrip()empieza por#y elcontinuesalta al principio del bucle sin tocartotales. - En la tercera vuelta,
'\n'pasa a''tras elstrip()y también se salta. - En la última vuelta, fíjate en
minutos: la lista por comprensión deja' carrera 'como'carrera', y por eso se suma a la misma clave (35 + 30 = 65). - Mira cómo
totalescrece: primero{'carrera': 35}, luego entra'bici'y por último'carrera'cambia de 35 a 65.
Resumen
- Leer y calcular van en funciones separadas: el archivo se recorre una vez y los datos viven en una lista o un diccionario.
- Cada línea se comprueba en orden: vacía o comentario, número de campos y valores convertibles. Y el aviso dice el número de línea.
- Para ampliar un archivo,
'a'; antes, comprueba que la última línea acaba en'\n'. - Valida antes de escribir: lo que entra mal en un archivo rompe a quien lo lea después.
- Para un archivo que se rehace entero (un informe),
'w'es lo correcto. Conos.makedirs()yos.path.join()preparas la carpeta y la ruta. - Comprueba lo escrito volviendo a leer el archivo.
Checklist: ¿lo has practicado?
- ¿Has ejecutado los tres programas y comprobado las salidas?
- ¿Sabes explicar por qué la línea 9 da aviso y la línea 6 no?
- ¿Sabrías decir qué habría escrito el Programa 2 sin
termina_en_salto()? - ¿Entiendes por qué el informe usa
'w'y el diario'a'? - ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
Si has respondido que sí a casi todo, estás listo para los ejercicios de archivos de texto, donde ya no te doy el código hecho. ¡Nos vemos allí!
Siguiente paso: ejerciciosArchivos de texto en Python — ejercicios con contadores de líneas, filtros de carreras y un diario que hay que normalizar (y la línea vacía que paró el bucle a la mitad)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
