Saltar a contenido

Lab guiado: Rutas y manejo de errores en ficheros


Cómo trabajar en parejas

Este lab se realiza en parejas conductor/navegante. Leed las instrucciones antes de empezar.

Roles

Rol Qué hace
Conductor Escribe el código/configuración y ejecuta los pasos. Solo el conductor toca el teclado.
Navegante Lee el enunciado en voz alta, propone soluciones, detecta errores y formula las preguntas.

Rotación

  • Cambiad de rol al final de cada paso.
  • Al rotar, el navegante ocupa el teclado y el conductor se convierte en navegante.
  • Ambos debéis poder explicar cualquier parte del trabajo al terminar el lab.

Entrega

Carpeta comprimida en formato zip que contenga todos los ficheros .py y las subcarpetas datos/, exportaciones/ y logs/ generadas durante el lab.


Antes de empezar: repasemos los conceptos

pathlib.Path Clase de Python que representa una ruta del sistema de ficheros como un objeto con atributos (name, stem, suffix, parent) y métodos propios (exists(), mkdir(), glob()…).

Operador / en pathlib Combina objetos Path para construir rutas de forma portable entre sistemas operativos — no uses concatenación de strings para rutas.

Path(__file__).parent Patrón habitual para obtener la carpeta donde está el script en ejecución. Permite construir rutas relativas que funcionan sin importar desde qué directorio se lanza el programa.

try / except Bloque que captura excepciones y permite reaccionar de forma controlada. Úsalo siempre que hagas I/O: el disco puede fallar, el fichero puede no existir, los permisos pueden ser insuficientes.

FileNotFoundError y PermissionError Las dos excepciones más habituales al trabajar con ficheros. Capturarlas por separado permite dar mensajes útiles al usuario y tomar decisiones diferentes en cada caso.

else y finally en try / except else se ejecuta solo si no hubo excepción — úsalo para la lógica de éxito. finally se ejecuta siempre — úsalo para limpieza, logs o contadores.


Paso 1 — Crear la estructura del proyecto 01_estructura.py

Conceptos

Path(__file__).parent devuelve la carpeta del script en ejecución. mkdir(parents=True, exist_ok=True) crea el directorio y todos los intermedios sin lanzar error si ya existe. Para escribir en un fichero con open(), usa el modo "w" (crea o sobreescribe):

with open(ruta, "w", encoding="utf-8") as f:
    f.write("línea 1\n")
    f.write("línea 2\n")

Objetivo

Crear con Python la estructura de carpetas del proyecto y generar los ficheros de datos de ejemplo que usaremos en el resto del lab.

Tarea

Copia este código en 01_estructura.py y ejecútalo:

from pathlib import Path

BASE = Path(__file__).parent

# Crear carpeta de datos
carpeta_datos = BASE / "datos"
carpeta_datos.mkdir(parents=True, exist_ok=True)

# Crear fichero de notas del grupo 1A
notas_1a = carpeta_datos / "notas_1a.txt"
with open(notas_1a, "w", encoding="utf-8") as f:
    f.write("Ana García,8.5\n")
    f.write("Carlos López,7.0\n")
    f.write("María Rodríguez,9.2\n")
    f.write("Pedro Martínez,6.8\n")

print(f"Creado: {notas_1a}")
print(f"¿Es fichero? {notas_1a.is_file()}")

Pregunta:

¿Qué error lanzaría mkdir() si no pasáramos exist_ok=True y la carpeta ya existiera?

Respuesta

Lanzaría FileExistsError. El parámetro exist_ok=True silencia ese error concreto — muy útil en scripts que se ejecutan varias veces.

Ejecuta el script: python 01_estructura.py. Comprueba que se ha creado la carpeta datos/ y el fichero notas_1a.txt.

TODO

Completa 01_estructura.py añadiendo lo que falta:

from pathlib import Path

BASE = Path(__file__).parent

datos = BASE / "datos"
datos.mkdir(parents=True, exist_ok=True)

notas_1a = datos / "notas_1a.txt"
notas_1a.write_text(
    "Ana García,8.5\nCarlos López,7.0\nMaría Rodríguez,9.2\nPedro Martínez,6.8\n",
    encoding="utf-8",
)
print(f"Creado: {notas_1a}")

# TODO: crea la carpeta 'exportaciones' dentro de BASE
# TODO: crea la carpeta 'logs' dentro de BASE

notas_1b = datos / "notas_1b.txt"
with open(notas_1b, "w", encoding="utf-8") as f:
    # TODO: escribe las cuatro líneas del grupo 1B (mismo formato: "Nombre,Nota\n")
    # Laura Sánchez,8.0 / Juan Pérez,5.5 / Isabel Torres,7.8 / Roberto Díaz,9.5
    pass

# TODO: comprueba con is_file() que notas_1b existe y muestra el resultado
Pista

Para crear exportaciones/ y logs/ usa el mismo patrón: (BASE / "carpeta_datos").mkdir(exist_ok=True). Para escribir notas_1b.txt abre el fichero con open(..., "w") igual que en el ejemplo.

Comprobación

Salida esperada:

Creado: .../datos/notas_1a.txt
Creado: .../datos/notas_1b.txt
¿Es fichero? True

Y en el sistema de ficheros deben existir las carpetas datos/, exportaciones/ y logs/.

Reflexión

Respuestas
  • Path(__file__).parent nos da la carpeta del script, no la de trabajo — el código es portable.
  • El operador / construye rutas sin concatenar strings, lo que funciona igual en Linux y Windows.
  • mkdir(exist_ok=True) hace el script idempotente: puedes ejecutarlo varias veces sin errores.
  • Cómo hemos visto anteriormente open(ruta, "w") crea o sobreescribe un fichero; f.write() escribe cada línea. El bloque with cierra el fichero automáticamente.

Paso 2 — Explorar propiedades de una ruta 02_rutas.py

Conceptos

Los objetos Path exponen atributos de solo lectura: name (nombre con extensión), stem (sin extensión), suffix (extensión con punto), parent (directorio padre). exists(), is_file() e is_dir() comprueban el tipo de elemento al que apunta la ruta.

Objetivo

Obtener información sobre rutas usando los atributos de Path, sin manipular strings manualmente.

Tarea

1
2
3
4
5
6
7
8
9
from pathlib import Path

ruta = Path("datos/alumnos/notas_1a.txt")

print(f"Nombre completo:  {ruta.name}")     # notas_1a.txt
print(f"Sin extensión:    {ruta.stem}")     # notas_1a
print(f"Extensión:        {ruta.suffix}")   # .txt
print(f"Directorio padre: {ruta.parent}")   # datos/alumnos
print(f"¿Existe?          {ruta.exists()}") # False (no la hemos creado)

Pregunta:

¿Por qué ruta.exists() devuelve False en el ejemplo anterior si datos/notas_1a.txt sí existe?

Respuesta

Porque la ruta que hemos le pasamos datos/alumnos/notas_1a.txt no existe en el disco duro. La que creamos fue datos/notas_1a.txt

Ejecuta el script: python 02_rutas.py.

TODO

from pathlib import Path

BASE = Path(__file__).parent
# Construye la ruta al fichero notas_1a.txt que creaste en el paso anterior
ruta = BASE / "datos" / "notas_1a.txt"

# TODO: muestra name, stem, suffix y parent de la ruta
# TODO: comprueba y muestra si la ruta existe, si es fichero y si es directorio
# TODO: comprueba si el directorio padre de la ruta existe con is_dir()
# TODO: comprueba y muestra si la ruta es absoluta. Investiga cómo se hace
# TODO: Muestra la ruta es absoluta. Investiga cómo se hace
Pista

ruta.parent devuelve otro objeto Path. Puedes encadenar: ruta.parent.parent para subir dos niveles, o ruta.parent.name para obtener solo el nombre del directorio padre.

Comprobación

Salida esperada:

Nombre completo:         notas_1a.txt
Sin extensión:           notas_1a
Extensión:               .txt
Directorio padre:        .../datos
Nombre directorio padre: datos
¿Existe?                 True
¿Es fichero?             True
¿Es directorio?          False
¿Padre existe?           True
¿Ruta absoluta?          True
Ruta absoluta            C:\Users\User\pd4\ut2-a2\lab\datos\notas_1a.txt

La ruta absoluta dependerá del caso particular en el que estés ejecutando el script.

Reflexión

Respuestas
  • Los atributos de Path son de solo lectura y no acceden al disco — son operaciones sobre la cadena de la ruta.
  • exists(), is_file() e is_dir() sí acceden al disco para comprobar el estado real.
  • Usar Path(__file__).parent en lugar de "." o rutas escritas directamente en el código hace el programa portable (ejecutable en otros equipos y sistemas operativos).

Paso 3 — Capturar FileNotFoundError 03_lectura.py

Conceptos

try / except permite reaccionar ante errores sin detener el programa. FileNotFoundError se lanza al abrir en modo "r" un fichero que no existe. La cláusula as e asigna la excepción a una variable para acceder al mensaje original del SO.

Objetivo

Leer un fichero de texto capturando el error de forma controlada si el fichero no existe.

Tarea

from pathlib import Path

BASE = Path(__file__).parent

try:
    with open(BASE / "datos" / "notas_inexistente.txt", "r", encoding="utf-8") as f:
        contenido = f.read()
    print(contenido)
except FileNotFoundError as e:
    print(f"Error: {e}")

Salida esperada:

Error: [Errno 2] No such file or directory: '.../datos/notas_inexistente.txt'

Pregunta:

¿Qué problema tiene capturar Exception en lugar de FileNotFoundError?

Respuesta

Exception captura cualquier error, incluidos bugs del programa (como un NameError o un TypeError). Esto silencia errores que deberían verse, haciendo imposible la depuración. Captura siempre el tipo más específico posible.

Ejecuta el script: python 03_lectura.py. Verifica que el mensaje de error aparece sin que el programa se detenga abruptamente.

TODO

from pathlib import Path

BASE = Path(__file__).parent
ruta = BASE / "datos" / "notas_1a.txt"

try:
    # TODO: abre el fichero en modo lectura y guarda las líneas en una variable
    pass
except FileNotFoundError:
    # TODO: muestra un mensaje claro indicando que el fichero no existe
    pass
else:
    # TODO: si la lectura tuvo éxito, muestra cuántas líneas contiene el fichero
    pass
Pista

Si quieres mostrar solo el nombre del fichero en el mensaje de error, usa ruta.name en lugar de mostrar la excepción completa con e.

Comprobación

Fichero leído: 4 líneas.

Si cambias la ruta a un fichero que no existe:

Error: el fichero 'notas_2a.txt' no existe.

Reflexión

Respuestas
  • try / except es el mecanismo estándar de Python para gestionar errores de I/O.
  • Capturar FileNotFoundError específicamente es mejor que Exception — solo interceptamos el caso que esperamos.
  • El bloque else (que añadiste en el TODO) se ejecuta solo si no hubo excepción — es el sitio correcto para la lógica de éxito.

Paso 4 — Capturar múltiples excepciones 04_excepciones.py

Conceptos

Varios bloques except permiten reaccionar de forma distinta ante cada tipo de error. Una tupla except (ErrorA, ErrorB) as e agrupa errores con la misma reacción. OSError es la clase base de FileNotFoundError, PermissionError e IsADirectoryError.

Objetivo

Distinguir entre tipos de error de I/O y mostrar mensajes útiles y específicos para cada caso.

Tarea

from pathlib import Path

BASE = Path(__file__).parent

def leer_fichero(ruta: Path):
    try:
        with open(ruta, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        print(f"'{ruta.name}' no existe.")
        return None
    except PermissionError:
        print(f"Sin permisos para leer '{ruta.name}'.")
        return None

contenido = leer_fichero(BASE / "datos" / "notas_1a.txt")
if contenido:
    print(f"Leído: {len(contenido)} caracteres.")
Pista

Para capturar cualquier error de fichero sin distinguir el tipo, usa OSError as e — es la clase base de FileNotFoundError, PermissionError e IsADirectoryError. str(e) incluye el código de error del SO y la ruta.

Pregunta:

¿Cuándo usarías except (FileNotFoundError, PermissionError) as e en lugar de dos bloques except separados?

Respuesta

Cuando quieres la misma reacción ante ambos errores — por ejemplo, mostrar el mensaje original del sistema con str(e) sin distinción. Si la reacción es diferente (crear el fichero si no existe, mostrar un mensaje al usuario indicando que no tiene permisos si no hay permisos), necesitas bloques separados.

Ejecuta: python 04_excepciones.py.

TODO

Antes de escribir el código, crea el fichero de prueba para el error de permisos:

  1. Crea el fichero datos/sin_permisos.txt con cualquier contenido.
  2. Con el botón derecho sobre el fichero en el explorador de archivos, quítale el permiso de lectura. Si no sabes cómo hacerlo, pregunta al profesor.
from pathlib import Path

BASE = Path(__file__).parent

def leer_fichero(ruta):
    try:
        with open(ruta, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        print(f"'{ruta.name}' no existe.")
        return None
    except PermissionError:
        print(f"Sin permisos para leer '{ruta.name}'.")
        return None

# TODO: llama a leer_fichero() con notas_1a.txt y muestra los caracteres leídos
# TODO: llama a leer_fichero() con un fichero que no existe y verifica el mensaje
# TODO: llama a leer_fichero() con datos/sin_permisos.txt (sin permiso de lectura)
# TODO: añade un tercer except que capture OSError para cualquier otro error de I/O
#       y muestre el mensaje original del SO usando 'as e'
# TODO: llama a leer_fichero(BASE / "datos") — la carpeta existe pero no se puede abrir como fichero

Comprobación

Leído: 52 caracteres.
'notas_2a.txt' no existe.
Sin permisos para leer 'sin_permisos.txt'.
Error de I/O al leer 'datos': [Errno 21] Is a directory: '.../datos'

Reflexión

Respuestas
  • Varios bloques except permiten reaccionar de forma diferente a cada tipo de error.
  • OSError como clase base captura cualquier error de I/O cuando no necesitamos distinguir el tipo exacto.
  • Envolver la lógica de I/O en una función devuelve None en caso de error, el código que llama decide qué hacer con eso.
  • El orden de los bloques except importa: Python ejecuta el primero que coincida. Si pones OSError antes que FileNotFoundError o PermissionError, estos nunca se alcanzan porque son subclases de OSError. Coloca siempre las excepciones más específicas primero.
  • IsADirectoryError (lanzada al abrir una carpeta como fichero) es también subclase de OSError pero no de PermissionError, por eso solo la captura el bloque except OSError.
  • Los permisos del sistema de ficheros son responsabilidad del SO, no de Python. Para reproducir un PermissionError en pruebas hay que quitarle el permiso de lectura al fichero desde fuera del programa.

Paso 5 — Patrón completo try / except / else / finally 05_patron_completo.py

Conceptos

else se ejecuta solo si no hubo excepción — separa la lógica de éxito de la de error. finally se ejecuta siempre — ideal para contadores, logs o cualquier limpieza garantizada.

Para escribir en un fichero con open(), el segundo argumento indica el modo:

# Modo "w" — crea o sobreescribe el fichero completo
with open(ruta, "w", encoding="utf-8") as f:
    f.write("contenido inicial\n")

# Modo "a" — añade al final sin borrar lo que ya hay
with open(ruta, "a", encoding="utf-8") as f:
    f.write("nueva línea\n")

Objetivo

Aplicar el patrón completo de cuatro bloques para leer y procesar notas de forma robusta, registrando cada intento en un fichero de log.

Tarea

from pathlib import Path

BASE = Path(__file__).parent
ruta = BASE / "datos" / "notas_1a.txt"
log = BASE / "logs" / "carga.log"
intentos = 0

try:
    with open(ruta, "r", encoding="utf-8") as f:
        lineas = f.readlines()
except FileNotFoundError:
    print("El fichero no existe. Iniciando con lista vacía.")
    lineas = []
else:
    print(f"Fichero cargado: {len(lineas)} registros.")
finally:
    intentos += 1
    with open(log, "a", encoding="utf-8") as f:
        f.write(f"Intento #{intentos} completado.\n")
    print(f"Intento de carga #{intentos} completado.")

Pregunta:

¿Qué diferencia hay entre poner código en else y ponerlo al final del bloque try?

Respuesta

else se ejecuta únicamente cuando el bloque try termina sin ninguna excepción. Por eso se usa para el flujo de éxito (por ejemplo, procesar datos ya leídos).

Si ese código se pone al final de try, queda dentro de la zona de riesgo: cualquier excepción en líneas anteriores o en ese propio código corta la ejecución y puede dejar trabajo a medias. Separarlo en else hace más claro qué parte es “intento” y qué parte es “éxito confirmado”.

Ejecuta: python 05_patron_completo.py.

TODO

from pathlib import Path

BASE = Path(__file__).parent
ruta = BASE / "datos" / "notas_1a.txt"
log = BASE / "logs" / "carga.log"
intentos = 0
resultado = ""

try:
    with open(ruta, "r", encoding="utf-8") as f:
        lineas = f.readlines()
except FileNotFoundError:
    print("El fichero no existe. Iniciando con lista vacía.")
    lineas = []
    resultado = "ERROR: fichero no encontrado"
else:
    notas = []
    for linea in lineas:
        # TODO: extrae la nota de cada línea (formato "Nombre,Nota")
        # y añádela a la lista 'notas' como float
        pass
    # TODO: calcula la media y muéstrala con dos decimales
    # TODO: asigna a 'resultado' algo como "OK: 4 registros, media 7.88"
    print(f"Fichero cargado: {len(lineas)} registros.")
finally:
    intentos += 1
    with open(log, "a", encoding="utf-8") as f:
        # TODO: escribe una línea con el número de intento y el valor de 'resultado'
        # Formato: "Intento #1 — OK: 4 registros, media 7.88\n"
        pass
    print(f"Intento de carga #{intentos} completado.")
Pista

Cada línea de notas_1a.txt tiene el formato Nombre,Nota. Para extraer la nota: _, nota_str = linea.strip().split(",") y luego float(nota_str).

Comprobación

Fichero cargado: 4 registros.
Nota media del grupo: 7.88
Intento de carga #1 completado.

El fichero logs/carga.log debe contener:

Intento #1 — OK: 4 registros, media 7.88

Reflexión

Respuestas
  • try / except / else / finally cubre los cuatro casos: intento, error, éxito y limpieza.
  • else hace el código más legible al separar el camino feliz del camino de error.
  • finally con escritura en log garantiza trazabilidad aunque el programa falle.

Paso 6 — Listar y procesar varios ficheros 06_listar.py

Conceptos

carpeta.glob("*.txt") devuelve un iterador de objetos Path filtrados por patrón. sorted() garantiza orden alfabético — glob() no garantiza ningún orden. Envuelve la lectura de cada fichero en su propio try / except para que un fichero corrupto no detenga el resto.

Objetivo

Procesar todos los ficheros .txt de la carpeta datos/ de forma robusta, calculando la nota media de cada grupo.

Tarea

1
2
3
4
5
6
7
from pathlib import Path

BASE = Path(__file__).parent
carpeta = BASE / "datos"

for fichero in sorted(carpeta.glob("*.txt")):
    print(f"  - {fichero.name}")

Pregunta:

Investiga qué permite iterdir() y contesta. ¿Qué ventaja tiene glob("*.txt") frente a iterdir() con un if para filtrar?

Respuesta

glob() expresa la intención directamente en el patrón — el código es más conciso y legible. Con iterdir() necesitarías algo como if f.suffix == ".txt" and f.is_file(), que es más "largo" y por tanto más propenso a que cometamos errores.

Ejecuta: python 06_listar.py. Verifica que aparecen los dos ficheros.

TODO

from pathlib import Path

BASE = Path(__file__).parent
carpeta = BASE / "datos"

for fichero in sorted(carpeta.glob("*.txt")):
    # TODO: lee el contenido del fichero con try/except OSError
    #       si falla, muestra el error y continúa con el siguiente (usa 'continue')
    try:
        pass # Completar
    except OSError as e:
        pass # Completar

    # TODO: parsea las líneas (formato "Nombre,Nota") y calcula la media
    # TODO: muestra "Grupo {fichero.stem}: media {media:.2f}"
Pista

Dentro del bucle, usa fichero.read_text(encoding="utf-8") para leer cada fichero. Envuelve esa llamada en try / except OSError para capturar cualquier error de lectura y continuar con el siguiente fichero usando continue.

Comprobación

Salida esperada si no hay errores:

Grupo notas_1a: media 7.88
Grupo notas_1b: media 7.70

Salida esperada si dentro de la carpeta hay un archivo sin permiso de lectura:

Error leyendo notas_2a.txt: [Errno 13] Permission denied: '.../datos/notas_2a.txt'
Grupo notas_1a: media 7.88
Grupo notas_1b: media 7.70

Salida esperada si un fichero no tiene ninguna nota válida:

  Aviso: línea ignorada en notas_1a.txt: 'Ana-8.5'
  Aviso: línea ignorada en notas_1a.txt: ''
Grupo notas_1a: sin datos válidos.
Grupo notas_1b: media 7.70

Reflexión

Respuestas
  • glob() combina listado y filtrado en una sola llamada — más expresivo que iterdir() + if.
  • sorted() asegura que el orden de procesamiento es siempre el mismo — importante para reproducibilidad.
  • continue dentro del except salta al siguiente fichero sin detener el bucle completo.

Paso 7 — Exportar resumen a fichero 07_exportar.py

Conceptos

write_text() sobreescribe el fichero completo — conveniente cuando el contenido se genera de una sola vez. Path.mkdir(exist_ok=True) garantiza que la carpeta de destino exista antes de escribir. Integra todos los patrones aprendidos: rutas relativas, glob(), try/except por fichero y escritura final.

Objetivo

Generar un fichero de resumen en exportaciones/resumen.txt con las medias de todos los grupos, integrando todo lo aprendido en el lab.

Tarea

from pathlib import Path
from datetime import date

BASE = Path(__file__).parent
carpeta_datos = BASE / "datos"
carpeta_export = BASE / "exportaciones"
carpeta_export.mkdir(exist_ok=True)

resultados = []

for fichero in sorted(carpeta_datos.glob("*.txt")):
    try:
        contenido = fichero.read_text(encoding="utf-8")
    except OSError as e:
        print(f"Error leyendo {fichero.name}: {e}")
        continue

    notas = [float(linea.split(",")[1]) for linea in contenido.strip().splitlines()]
    media = sum(notas) / len(notas)
    resultados.append(f"{fichero.stem}: {media:.2f}")

resumen = f"Resumen de notas — {date.today()}\n" + "\n".join(resultados) + "\n"

salida = carpeta_export / "resumen.txt"
salida.write_text(resumen, encoding="utf-8")
print(f"Resumen exportado a: {salida.name}")
print(resumen)

Pregunta:

¿Por qué usamos read_text() en este script en lugar de open() con with?

Respuesta

Porque leemos todo el contenido de una sola vez y no necesitamos acceso línea a línea ni escritura incremental. read_text() es más conciso en ese caso. Si tuviéramos que procesar ficheros muy grandes línea a línea, open() con with sería la opción correcta para no cargar todo en memoria.

Ejecuta: python 07_exportar.py. Abre exportaciones/resumen.txt y verifica el contenido.

TODO

from pathlib import Path
from datetime import date

BASE = Path(__file__).parent
carpeta_datos = BASE / "datos"
carpeta_export = BASE / "exportaciones"
carpeta_export.mkdir(exist_ok=True)

resultados = []

for fichero in sorted(carpeta_datos.glob("*.txt")):
    try:
        contenido = fichero.read_text(encoding="utf-8")
    except OSError as e:
        print(f"Error leyendo {fichero.name}: {e}")
        continue

    notas = []
    for linea in contenido.strip().splitlines():
        # TODO: añade try/except ValueError para saltar líneas con formato incorrecto
        #       y mostrar un aviso indicando qué línea falló
        _, nota_str = linea.split(",")
        notas.append(float(nota_str))

    if not notas:
        continue

    media = sum(notas) / len(notas)
    resultados.append(f"{fichero.stem}: {media:.2f}")

# TODO: construye el string 'resumen' con la fecha de hoy y los resultados
# TODO: escribe el resumen en exportaciones/resumen.txt con write_text()
# TODO: muestra por pantalla la ruta del fichero exportado y su contenido
Pista

Si una línea está vacía o no tiene coma, split(",") devolverá una lista con menos de dos elementos y lanzará ValueError al desempaquetar. Captura ese error dentro del bucle de líneas con try / except ValueError y usa continue para saltarla.

Comprobación

Salida esperada si no hay errores

Resumen exportado a: resumen.txt

Resumen de notas — 2026-03-05
notas_1a: 7.88
notas_1b: 7.70
Puedes añadir una sección así, justo después de Comprobación:

Salida esperada si le quitamos permisos de lectura a notas_1b.txt.

Error leyendo notas_1b.txt: [Errno 13] Permission denied: '/.../datos/notas_1b.txt'
Resumen exportado a: resumen.txt

Resumen de notas — 2026-03-06
notas_1a: 7.88
Salida esperada con línea errónea en notas_1a.tx: Ana 8.5 (sin coma).

Aviso: línea ignorada en notas_1a.txt: 'Ana 8.5'
Resumen exportado a: resumen.txt

Resumen de notas — 2026-03-06
notas_1a: 7.60
notas_1b: 7.70

Salida esperada si todas las líneas fallan en un fichero, ese grupo no aparece en el resumen final.

Aviso: línea ignorada en notas_1b.txt: '---'
Aviso: línea ignorada en notas_1b.txt: ''
Resumen exportado a: resumen.txt

Resumen de notas — 2026-03-06
notas_1a: 7.88

Reflexión

Respuestas
  • Path.mkdir(exist_ok=True) garantiza que la carpeta de destino exista antes de escribir — siempre hazlo.
  • try / except por fichero dentro del bucle aísla los fallos: un fichero corrupto no detiene el proceso.
  • continue en el except es el patrón habitual para procesar colecciones con tolerancia a fallos.
  • write_text() es suficiente cuando el contenido se genera completo en memoria. Para ficheros grandes, usa open().

Kata 1 — Rutas más expresivas

En 07_exportar.py, el nombre del grupo en el resumen viene de fichero.stem. Modifica el script para que el nombre sea la parte después del último guion bajo: notas_1a1A. Usa los métodos de string de Python.

Kata 2 — Log acumulativo

En 05_patron_completo.py usaste write_text() para el log, lo que sobreescribe en cada ejecución. Cámbialo para que añada líneas al log sin borrar las anteriores. Pista: abre con open(log, "a").

Kata 3 — Validación estricta (avanzado)

En 07_exportar.py, si una nota está fuera del rango [0, 10], considera la línea inválida y omítela del cálculo. Añade al resumen final cuántas líneas inválidas se encontraron en cada fichero.


Checklist de entrega

  • He creado la estructura de directorios del proyecto usando pathlib y Path(__file__).parent.
  • He inspeccionado rutas con name, stem, suffix, parent, exists() e is_file().
  • He capturado FileNotFoundError y PermissionError con bloques except separados.
  • He aplicado el patrón completo try / except / else / finally con lógica de log.
  • He procesado varios ficheros con glob() capturando errores por fichero con continue.
  • He exportado un resumen usando write_text() y he validado el fichero resultante.
  • He evitado concatenar rutas con strings usando el operador / de pathlib.

Actividades de ampliación

A — Leer con csv.reader (requiere paso 7)

El módulo csv de la librería estándar maneja correctamente nombres con comas, espacios y comillas.

Fichero: ampliacion_a.py

import csv
from pathlib import Path

BASE = Path(__file__).parent
ruta = BASE / "datos" / "notas_1a.txt"

with open(ruta, "r", encoding="utf-8", newline="") as f:
    lector = csv.reader(f)
    for fila in lector:
        # tu código aquí — fila es una lista: ['Ana García', '8.5']
        pass

¿Qué ocurre si el nombre de un alumno contiene una coma? ¿Cómo lo manejaría csv.reader frente a un split(",")?


B — Buscar alumno por nombre (requiere paso 6)

Implementa una función buscar_alumno(nombre) que busque un alumno en todos los ficheros de datos/ y devuelva una lista de tuplas (grupo, nota).

Fichero: ampliacion_b.py

from pathlib import Path

def buscar_alumno(nombre):
    BASE = Path(__file__).parent
    resultados = []
    for fichero in sorted((BASE / "datos").glob("*.txt")):
        # tu código aquí
        pass
    return resultados

print(buscar_alumno("Ana García"))
# Salida esperada: [('notas_1a', 8.5)]

C — Comparativa pathlib vs os.path (requiere paso 2)

Reescribe 02_rutas.py usando exclusivamente os.path en lugar de pathlib. Crea una tabla comparativa en un comentario al final del fichero con las equivalencias que hayas encontrado.

Fichero: ampliacion_c.py

import os.path

ruta = "datos/alumnos/notas_1a.txt"

# tu código aquí — replica lo que hace 02_rutas.py pero con os.path
# os.path.basename(), os.path.splitext(), os.path.dirname(), os.path.exists()...

# TABLA COMPARATIVA:
# | Operación        | pathlib          | os.path                    |
# |------------------|------------------|----------------------------|
# | Nombre fichero   | ruta.name        | os.path.basename(ruta)     |
# | ...              | ...              | ...                        |

D — Historial de exportaciones (requiere paso 7)

Modifica 07_exportar.py para que el fichero de salida incluya un timestamp en el nombre: resumen_20260305_143022.txt. Así cada exportación genera un fichero nuevo sin sobreescribir el anterior.

from datetime import datetime
# tu código aquí
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
nombre_salida = f"resumen_{timestamp}.txt"

¿Cómo limpiarías exportaciones antiguas para no acumular ficheros indefinidamente?


E — Reto final: gestor interactivo de notas

Crea un programa gestor.py que presente un menú de texto en bucle con las siguientes opciones:

1. Listar grupos disponibles
2. Ver notas de un grupo
3. Añadir nota a un grupo
4. Exportar resumen general
5. Salir

Requisitos: - Usa pathlib para todas las rutas. - Captura errores de I/O en cada operación con mensajes claros. - La opción 3 debe añadir la línea al fichero sin borrar las existentes. - La opción 4 genera exportaciones/resumen_YYYYMMDD_HHMMSS.txt. - No uses ninguna librería externa — solo la librería estándar de Python.