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.

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 que strip() lo quita (junto con espacios y tabuladores de los extremos).
  • Recorrer un archivo con for linea in f y escribir con print(..., file=f) y write().
  • 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:

  1. leer_sesiones() recorre el archivo con for y enumerate(), 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.
  2. minutos_por_deporte() convierte la lista de sesiones en un diccionario deporte -> minutos.
  3. 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 el strip(), la línea vacía sí es '', y el continue la salta junto al comentario.
  • len(partes) != 3 se comprueba antes de desempaquetar. fecha, deporte, minutos = ['2026-10-13', 'carrera'] daría ValueError: 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 el ValueError de int('veinte'). isdigit() es True solo si todos los caracteres son dígitos.

Esta es la traza de las diez líneas:

LíneaTras strip()Decisiónsesiones / 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() falsoaviso de la línea 7
8'2026-10-12;fútbol;90'válida+ (…, 'fútbol', 90)
9'2026-10-13;carrera'solo 2 camposaviso 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:

El Programa 1 línea a línea Las diez líneas de entrenos.txt, qué hace el programa con cada una y el diccionario de totales que resulta. De las líneas del archivo al diccionario 1 # diario de entrenamientos: …⏎ comentario 2 2026-10-05;carrera;35⏎ +35 3 2026-10-06;bici;50⏎ +50 4 ⏎ vacía 5 2026-10-08;natación;40⏎ +40 6 2026-10-09; carrera ;30 ⇥⏎ +30 (strip) 7 2026-10-11;yoga;veinte⏎ aviso 8 2026-10-12;fútbol;90⏎ +90 9 2026-10-13;carrera⏎ aviso 10 2026-10-14;bici;45⏎ +45 totales ‘bici’: 95 ‘carrera’: 65 ‘fútbol’: 90 ‘natación’: 40 Total: 290 min estrella: bici (95) 2 avisos con su número de línea
Cada línea acaba en una de tres casillas: se suma al diccionario, se salta sin más o deja un aviso con su número.

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 conjunto DEPORTES.
  • 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:

  1. termina_en_salto() lee el archivo (es pequeño) y comprueba el último carácter.
  2. registrar() valida, añade el salto si hace falta y escribe la sesión con su '\n'.
  3. 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 DEPORTES o los minutos no son positivos, no se escribe nada y se devuelve False. Mejor no apuntar que apuntar basura que luego rompe el resumen.
  • salto = '' if termina_en_salto(nombre) else '\n' es un if en una línea: salto vale '' 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:

VueltaSesió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:

El salto que falta Un diario cuya última línea no termina en salto, y el resultado de añadir una sesión sin comprobarlo y comprobándolo. Añadir con ‘a’ a un archivo sin salto final diario.txt antes 2026-10-12;fútbol;90⏎ 2026-10-14;bici;45 ← sin ⏎ write(‘2026-10-15;carrera;40\n’) lo que quieres añadir Sin comprobar 2026-10-12;fútbol;90⏎ 2026-10-14;bici;452026-10-15;carrera;40⏎ Dos sesiones pegadas en una línea Con termina_en_salto() 2026-10-12;fútbol;90⏎ 2026-10-14;bici;45 ⏎ 2026-10-15;carrera;40⏎ salto = ‘\n’ antes de la sesión nueva
‘a’ escribe justo detrás del último carácter: si la última línea no acaba en salto, la sesión nueva se le pega.

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:

Tubería del Programa 3 Cuatro cajas: el archivo de entrada, la lista de sesiones válidas, el diccionario resumen y el archivo del informe. Del diario al informe: leer, resumir, escribir entrenos.txt 10 líneas open(…, ‘r’) validas 6 tuplas (‘bici’, 50)… datos 4 deportes ‘bici’: [2, 95] informes/octubre.txt 10 líneas open(…, ‘w’) leer_validas() resumen() escribir_informe() Archivo → lista → diccionario → archivo: cada función hace una sola cosa.
El programa lee con ‘r’, trabaja con estructuras en memoria y solo al final abre el informe con ‘w’ para escribirlo entero.

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óndatos 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}' y f'{deporte:<10}{s:>9}{m:>9}' usan los mismos anchos (10, 9 y 9), y por eso las columnas cuadran.
  • total_s = total_m = 0 pone 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 mes en 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 da ValueError.
  • 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:

  1. En la primera vuelta, linea es el comentario: tras el strip() empieza por # y el continue salta al principio del bucle sin tocar totales.
  2. En la tercera vuelta, '\n' pasa a '' tras el strip() y también se salta.
  3. 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).
  4. Mira cómo totales crece: 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. Con os.makedirs() y os.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 →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *