Contenido del curso
Fundamentos de Python
Los fundamentos de Python incluyen la sintaxis (sangría para bloques de código), los tipos de datos básicos (numéricos, booleanos, cadenas de texto), las variables, el control de flujo (condicionales como if/elif/else y bucles como for/while), y las funciones (bloques de código reutilizables).
0/6
Operadores y Control de Flujo
Los operadores en Python se clasifican en varios tipos (aritméticos, de comparación, lógicos, de asignación, etc.), mientras que el control de flujo se refiere al orden en que se ejecutan las instrucciones, modificándolo con estructuras como if, elif, else (condicionales), y for o while (bucles). Las instrucciones break, continue y pass también controlan el flujo dentro de los bucles.
0/3
Funciones y Manejo de Errores
Las funciones en Python son bloques de código reutilizables, mientras que el manejo de errores (excepciones) se hace con los bloques try, except, else y finally para gestionar errores de ejecución y evitar que el programa se detenga abruptamente. try ejecuta un código, except lo captura si ocurre un error específico, else se ejecuta si no hay error y finally se ejecuta siempre, haya o no error.
0/3
Estructuras de Datos
Las estructuras de datos principales en Python son las listas, tuplas, diccionarios y conjuntos. Estos tipos de datos se diferencian por su mutabilidad (si sus elementos se pueden cambiar después de su creación) y si mantienen el orden de los elementos. Las listas son ordenadas y mutables, mientras que las tuplas son ordenadas e inmutables. Los diccionarios son colecciones no ordenadas de pares clave-valor, y los conjuntos son colecciones desordenadas de elementos únicos.
0/5
Programación Orientada a Objetos (POO)
La Programación Orientada a Objetos (POO) en Python es un paradigma que organiza el código en torno a objetos, que son instancias de clases. Las clases actúan como plantillas que definen los atributos (datos) y métodos (comportamientos) de los objetos, permitiendo crear programas más modularizados, reutilizables y fáciles de mantener. Python soporta conceptos clave de la POO como la herencia, el encapsulamiento y el polimorfismo.
0/5
Ambientes virtuales
Un entorno virtual de Python es un espacio aislado que permite instalar paquetes y dependencias específicos para un proyecto concreto sin afectar a otras aplicaciones o a la instalación global de Python. Se crea una carpeta con una instalación de Python y una copia local de pip dentro de este entorno, lo que permite a cada proyecto tener sus propias bibliotecas y versiones, evitando así conflictos entre diferentes proyectos que puedan requerir versiones distintas de la misma librería.
0/1
Archivos
El manejo de archivos en Python se realiza principalmente usando la función open() para abrir un archivo y los métodos read(), write(), append() y close() para manipularlo. Es crucial gestionar los archivos adecuadamente, cerrándolos para liberar recursos, aunque es más recomendable usar la sentencia with, que cierra el archivo automáticamente. Python permite trabajar con archivos de texto y binarios, así como con distintos modos de apertura como 'r' (solo lectura), 'w' (escritura/sobreescritura), y 'a' (añadir).
0/1
Módulos y Librerías Estándar
Un "módulo" en Python se refiere a dos conceptos distintos: un archivo .py con código que se puede importar para reutilizar funciones, clases y variables, y el operador % que calcula el residuo de una división entera. Ambos son útiles para organizar el código y resolver problemas matemáticos, respectivamente.
0/2
Hilos y tareas en Python
En Python, los hilos (threads) son secuencias de ejecución dentro de un proceso que permiten la concurrencia, ejecutando tareas simultáneamente para aprovechar mejor los recursos del sistema. Las tareas son las unidades de trabajo a realizar, como descargar archivos o procesar datos. Se utilizan para manejar operaciones que implican espera (I/O-bound) de forma eficiente, permitiendo que una aplicación no se bloquee mientras espera. Para ello, se usa el módulo threading, se crean objetos Thread que representan las tareas, se inician con .start() y se pueden sincronizar con mecanismos como Lock para evitar conflictos.
0/1
Curso de Programación en Pythón 3.




    💡 Nota: Este módulo te enseñará a escribir programas que no se detienen ante los errores, sino que los manejan inteligentemente.
    Aprenderás cómo capturar, identificar, prevenir y lanzar tus propias excepciones, además de técnicas para depurar y verificar el correcto funcionamiento de tu código.



    Parte 1 — Introducción al Manejo de Excepciones en Python

    En cualquier lenguaje de programación, los errores son inevitables.
    Sin embargo, un buen programador no busca evitarlos por completo, sino anticiparlos y manejarlos correctamente.
    En Python, esta tarea se realiza mediante el sistema de excepciones.


    1.1 ¿Qué es una excepción?

    Una excepción es un evento que ocurre durante la ejecución de un programa y que interrumpe el flujo normal de las instrucciones.
    Cuando Python encuentra una situación inesperada (como dividir entre cero o intentar abrir un archivo inexistente), genera una excepción y detiene el programa.

    # Ejemplo: división entre cero
    print("Inicio del programa")
    resultado = 10 / 0
    print("Fin del programa")

    Salida:

    Inicio del programa  
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
    ZeroDivisionError: division by zero

    El programa se detiene en el momento del error y muestra una traza del error (traceback),
    indicando el tipo de excepción y la línea donde ocurrió.

    💡 Dato: Las excepciones en Python son objetos especiales derivados de la clase base Exception.

    1.2 Por qué es importante manejarlas

    Manejar las excepciones permite que tu programa sea más estable, seguro y profesional.
    Un sistema robusto no se detiene ante un error; en cambio, lo detecta, lo comunica y continúa funcionando.

    Imagina que tu aplicación web deja de funcionar porque un usuario introdujo un valor inválido.
    Con manejo de excepciones, puedes mostrar un mensaje amistoso en lugar de un fallo técnico.

    try:
        numero = int(input("Introduce un número entero: "))
        print(f"Has introducido: {numero}")
    except ValueError:
        print("Error: Debes introducir un número válido.")

    Salida:

    Introduce un número entero: hola  
    Error: Debes introducir un número válido.

    El programa no se detiene; simplemente reacciona ante la excepción y sigue funcionando.


    1.3 Tipos de errores en Python

    En Python distinguimos dos grandes categorías de errores:

    Tipo de errorDescripciónEjemplo
    Error de sintaxisOcurre cuando el código no sigue las reglas del lenguaje.print("Hola" → falta paréntesis)
    Excepción en tiempo de ejecuciónOcurre cuando el programa se ejecuta correctamente, pero surge una situación inesperada.10 / 0ZeroDivisionError

    Las excepciones son, por tanto, errores que ocurren durante la ejecución y pueden ser manejadas.


    1.4 Ejemplo de ejecución sin manejo de errores

    Veamos un ejemplo sin manejo de errores:

    def dividir(a, b):
        return a / b
    
    print(dividir(10, 2))
    print(dividir(5, 0))
    print("Este mensaje nunca se mostrará")

    Salida:

    5.0  
    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
    ZeroDivisionError: division by zero

    El programa se detiene en la línea donde ocurre el error.


    1.5 Ejemplo con manejo de excepciones

    def dividir(a, b):
        try:
            resultado = a / b
            return resultado
        except ZeroDivisionError:
            print("Error: No se puede dividir entre cero.")
            return None
    
    print(dividir(10, 2))
    print(dividir(5, 0))
    print("El programa sigue ejecutándose correctamente.")

    Salida:

    5.0  
    Error: No se puede dividir entre cero.  
    El programa sigue ejecutándose correctamente.

    Ahora el programa controla el fallo, muestra un mensaje y continúa su ejecución sin detenerse.


    1.6 Ventajas del manejo de excepciones

    • ✅ Permite controlar el flujo del programa ante errores imprevistos.
    • ✅ Mejora la experiencia del usuario final.
    • ✅ Facilita la depuración y el mantenimiento del código.
    • ✅ Evita pérdidas de datos o comportamientos inesperados.
    • ✅ Aumenta la robustez de aplicaciones grandes o críticas.

    1.7 Buenas prácticas iniciales

    • ✅ Usa siempre bloques try y except para operaciones propensas a error (divisiones, conversiones, archivos).
    • ✅ Específica el tipo exacto de excepción (por ejemplo, ValueError o ZeroDivisionError).
    • ✅ No uses except: sin tipo — puede ocultar errores graves.
    • ✅ Agrega mensajes descriptivos para facilitar el diagnóstico.
    • ✅ Evita abusar del manejo de excepciones para controlar la lógica normal del programa.

    1.8 Mini-quiz

    1. ¿Qué es una excepción?
    2. ¿Cuál es la diferencia entre un error de sintaxis y una excepción?
    3. ¿Por qué es importante manejar los errores?
    💡 Ver respuestas
    • 1️⃣ Es un evento inesperado que interrumpe el flujo normal del programa.
    • 2️⃣ El error de sintaxis ocurre antes de ejecutar; la excepción ocurre durante la ejecución.
    • 3️⃣ Porque permite mantener el programa funcionando sin interrupciones.

    1.9 Prácticas guiadas

    Ejercicio 1 — Conversión segura

    try:
        numero = int(input("Introduce un número entero: "))
        print(f"Número válido: {numero}")
    except ValueError:
        print("Debes ingresar un número entero válido.")

    Ejercicio 2 — División segura

    def dividir_seguro(a, b):
        try:
            return a / b
        except ZeroDivisionError:
            return "Error: división entre cero."
    
    print(dividir_seguro(10, 0))

    Ejercicio 3 — Multiples operaciones

    try:
        x = int(input("Número 1: "))
        y = int(input("Número 2: "))
        print("Resultado:", x / y)
    except ValueError:
        print("Introduce solo números.")
    except ZeroDivisionError:
        print("No se puede dividir entre cero.")
    finally:
        print("Fin de la operación.")



    Parte 2 — Errores comunes en Python

    Antes de aprender a capturar y manejar excepciones, es esencial conocer los errores más frecuentes que pueden aparecer al programar en Python.
    Comprenderlos te ayudará a anticiparte a ellos y a escribir código más sólido y confiable.


    2.1 Tipos generales de errores

    Podemos clasificar los errores en dos categorías principales:

    TipoCuándo ocurreEjemplo
    Errores de sintaxisCuando el código viola las reglas del lenguaje.print("Hola" → Falta el paréntesis de cierre)
    Excepciones en tiempo de ejecuciónCuando el código es sintácticamente correcto pero falla al ejecutarse.10 / 0ZeroDivisionError

    2.2 Errores de sintaxis

    Los errores de sintaxis (SyntaxError) se detectan antes de ejecutar el programa.
    Indican que hay un problema con la estructura del código: faltan paréntesis, comillas o dos puntos, por ejemplo.

    # Error de sintaxis: falta el paréntesis de cierre
    print("Hola mundo"

    Salida:

      File "<stdin>", line 1
        print("Hola mundo"
                         ^
    SyntaxError: unexpected EOF while parsing

    Este tipo de error se corrige antes de ejecutar el código.
    Los editores modernos (como VS Code o PyCharm) suelen marcarlo automáticamente.


    2.3 Excepciones en tiempo de ejecución

    Las excepciones ocurren mientras el programa está en marcha.
    Aunque el código sea correcto, una operación puede fallar por causas imprevistas.

    # División entre cero
    resultado = 10 / 0
    print(resultado)

    Salida:

    ZeroDivisionError: division by zero

    En este caso, Python detecta un intento de dividir entre cero y lanza una excepción.


    2.4 Excepciones más comunes en Python

    A continuación, una tabla con los errores más frecuentes que encontrarás al programar:

    ExcepciónDescripciónEjemplo
    ValueErrorEl tipo de dato es correcto, pero el valor no tiene sentido.int("hola")
    TypeErrorSe intenta operar entre tipos incompatibles."3" + 2
    ZeroDivisionErrorSe intenta dividir entre cero.10 / 0
    IndexErrorÍndice fuera de rango en una lista o tupla.lista = [1, 2]; print(lista[5])
    KeyErrorSe accede a una clave inexistente en un diccionario.dic = {"a": 1}; print(dic["b"])
    FileNotFoundErrorSe intenta abrir un archivo que no existe.open("archivo.txt")
    AttributeErrorSe llama a un atributo o método que no existe.numero = 10; numero.append(5)
    NameErrorSe usa una variable que no ha sido definida.print(variable_inexistente)
    ImportErrorEl módulo que intentas importar no se encuentra.import modulo_que_no_existe
    MemoryErrorEl sistema se queda sin memoria.[0] * 999999999999

    2.5 Ejemplos prácticos

    Ejemplo 1 — ValueError

    numero = int("abc")

    Salida:

    ValueError: invalid literal for int() with base 10: 'abc'

    Ejemplo 2 — TypeError

    print("Edad: " + 25)

    Salida:

    TypeError: can only concatenate str (not "int") to str

    Ejemplo 3 — IndexError

    lista = [1, 2, 3]
    print(lista[5])

    Salida:

    IndexError: list index out of range

    Ejemplo 4 — FileNotFoundError

    f = open("archivo_inexistente.txt")

    Salida:

    FileNotFoundError: [Errno 2] No such file or directory: 'archivo_inexistente.txt'

    2.6 Cómo identificar el tipo de excepción

    Cuando ocurre un error, Python muestra el nombre del tipo de excepción en el mensaje de error.
    Esto te ayuda a saber qué tipo de problema ocurrió para manejarlo correctamente con except.

    try:
        x = int("Hola")
    except Exception as e:
        print("Se produjo un error de tipo:", type(e).__name__)

    Salida:

    Se produjo un error de tipo: ValueError

    2.7 Cómo prevenir errores comunes

    • ✅ Verifica los datos de entrada antes de convertir tipos (int(), float()…).
    • ✅ Comprueba que un archivo existe antes de abrirlo.
    • ✅ Asegúrate de que los índices están dentro del rango de la lista.
    • ✅ Evita dividir entre cero comprobando el divisor previamente.
    • ✅ Define todas las variables antes de usarlas.
    • ✅ Importa correctamente los módulos y dependencias.

    2.8 Mini-quiz

    1. ¿Qué diferencia hay entre un SyntaxError y un ValueError?
    2. ¿Qué tipo de error ocurre si intentas acceder a un índice inexistente de una lista?
    3. ¿Qué error se lanza al dividir entre cero?
    💡 Ver respuestas
    • 1️⃣ El primero ocurre antes de ejecutar el programa; el segundo durante su ejecución.
    • 2️⃣ IndexError.
    • 3️⃣ ZeroDivisionError.

    2.9 Prácticas guiadas

    Ejercicio 1 — Detección de errores

    Identifica y corrige los errores en el siguiente código:

    lista = [1, 2, 3]
    print(lista[3])
    print(10 / 0)
    valor = int("Hola")

    Ejercicio 2 — Mostrar tipo de excepción

    try:
        resultado = 10 / 0
    except Exception as e:
        print("Error detectado:", type(e).__name__)

    Ejercicio 3 — Comprobación previa

    numerador = 10
    denominador = 0
    
    if denominador != 0:
        print("Resultado:", numerador / denominador)
    else:
        print("Error: no se puede dividir entre cero.")



    Parte 3 — Bloques try, except, else, finally

    El manejo de excepciones en Python se basa en cuatro piezas clave:
    try (intenta ejecutar), except (captura errores),
    else (se ejecuta si no hubo errores) y finally
    (se ejecuta siempre, ocurra o no una excepción). Usadas con cabeza, hacen tu código robusto, legible y mantenible.


    3.1 Visión general

    BloqueCuándo se ejecutaUso típico
    trySiempre; aquí pones el código que puede fallarOperaciones frágiles (I/O, parseo, divisiones, red)
    exceptSolo si ocurre una excepción en el tryManejar casos de error específicos (p. ej., ValueError)
    elseSolo si no ocurrió ninguna excepciónTrabajos que dependen del éxito del try
    finallySiempre, haya o no excepciónLiberar recursos (cerrar archivos, conexiones, locks)

    3.2 Sintaxis básica

    try:
        # Código que podría lanzar una excepción
        resultado = 10 / x
    except ZeroDivisionError:
        # Se ejecuta si pasó ZeroDivisionError
        print("No se puede dividir entre cero.")
    

    Con else y finally:

    try:
        resultado = 10 / x
    except ZeroDivisionError:
        print("No se puede dividir entre cero.")
    else:
        # Solo si no hubo excepción
        print("Resultado:", resultado)
    finally:
        # Siempre
        print("Fin del proceso (liberar recursos, etc.)")
    
    💡 Regla de oro: usa else para lo que depende del éxito del try
    y finally para limpieza. Mantén el try lo más pequeño posible (solo lo que puede fallar).

    3.3 Múltiples except y captura de varias excepciones

    Captura tipos específicos primero; de lo contrario, el bloque general “tapa” a los concretos.

    try:
        n = int(input("Número: "))
        print(10 / n)
    except ValueError:
        print("Debes escribir un número entero.")
    except ZeroDivisionError:
        print("No se puede dividir entre cero.")
    except Exception as e:
        print("Error inesperado:", type(e).__name__)
    

    O agrupa varias excepciones relacionadas en una tupla:

    try:
        # lectura y conversión
        dato = float(input("Precio: "))
    except (ValueError, TypeError) as e:
        print("Entrada inválida:", e)
    

    3.4 Captura de la excepción como objeto

    Accede al mensaje/atributos del error con as e para loguear o mostrar detalles.

    try:
        with open("config.yaml") as f:
            contenido = f.read()
    except FileNotFoundError as e:
        print("Archivo no encontrado:", e.filename)
    

    3.5 Uso correcto de else

    else se ejecuta solo si todo fue bien en el try. Úsalo para evitar capturar errores de código que no debe estar en el try.

    try:
        n = int(input("Edad: "))
    except ValueError:
        print("Edad inválida.")
    else:
        # Solo si la conversión fue OK
        categoria = "Mayor" if n >= 18 else "Menor"
        print("Categoría:", categoria)
    

    3.6 Uso correcto de finally (limpieza)

    Se usa para liberar recursos siempre: cerrar archivos, conexiones, deshacer locks…

    f = None
    try:
        f = open("datos.txt")
        datos = f.read()
        # procesar datos...
    except FileNotFoundError:
        print("No existe el archivo.")
    finally:
        if f:
            f.close()  # se ejecuta ocurra o no la excepción
    

    Con context manager (with) no necesitas finally para cerrar:

    try:
        with open("datos.txt") as f:
            datos = f.read()
    except FileNotFoundError:
        print("No existe el archivo.")
    
    Preferible: usa with siempre que exista un context manager. Simplifica y evita fugas de recursos.

    3.7 Anidación y re-lanzamiento (raise)

    Puedes capturar, registrar y volver a lanzar (para que otro nivel lo maneje):

    def cargar_config(ruta):
        try:
            with open(ruta) as f:
                return f.read()
        except FileNotFoundError as e:
            print("[LOG] faltante:", e.filename)
            raise  # re-lanza la misma excepción
    
    try:
        cfg = cargar_config("app.cfg")
    except FileNotFoundError:
        print("Config no encontrada. Usando valores por defecto.")
    

    3.8 Patrones recomendados (y anti-patrones)

    Haz ✅No hagas ❌Por qué
    Captura excepciones específicasexcept Exception: para todoLo genérico oculta bugs difíciles
    Minimiza el bloque tryEnvolver medio archivo en tryAcota el punto de fallo y mejora legibilidad
    Usa else para post-éxitoPoner lógica “feliz” dentro del tryEvitas capturar errores que no deben
    Usa finally o withOlvidar liberar recursosPreviene fugas, locks colgados, archivos abiertos
    Ordena de específico → generalGenérico antes que específicoEl general “come” a los específicos

    3.9 Flujo completo con try/except/else/finally

    def dividir(a: float, b: float) -> float | None:
        f = None
        try:
            # Solo lo frágil
            resultado = a / b
        except ZeroDivisionError:
            print("No se puede dividir entre cero.")
            return None
        else:
            # Éxito: ya puedes usar 'resultado'
            return resultado
        finally:
            # Siempre
            # (Si tuvieras recursos abiertos, ciérralos aquí)
            pass
    
    print(dividir(10, 2))  # 5.0
    print(dividir(10, 0))  # None
    

    3.10 Ejemplos prácticos

    3.10.1 Lectura segura de archivo + parseo

    def leer_entero(ruta):
        try:
            with open(ruta) as f:
                valor = int(f.read().strip())
        except FileNotFoundError:
            print("Archivo no encontrado.")
            return None
        except ValueError:
            print("El contenido no es un entero válido.")
            return None
        else:
            return valor
    
    print(leer_entero("input.txt"))
    

    3.10.2 Red: reintentos básicos

    import time, random
    
    def fetch():
        # Simulación: falla 2 de cada 3 veces
        if random.choice([True, False, False]):
            raise ConnectionError("Fallo de red simulado")
        return "OK"
    
    def fetch_con_reintentos(max_reintentos=3):
        for intento in range(1, max_reintentos + 1):
            try:
                return fetch()
            except ConnectionError as e:
                print(f"Intento {intento}: {e}")
                time.sleep(0.5)
        return None
    
    print(fetch_con_reintentos())
    

    3.11 Mini-quiz

    1. ¿Cuándo se ejecuta el bloque else de un try?
    2. ¿Por qué conviene ordenar los except del más específico al más general?
    3. Completa para capturar dos tipos a la vez:
      try:
          x = int("hola"); print(10/x)
      except (__________, __________) as e:
          print("Error:", type(e).__name__)
      Respuesta

      ValueError, ZeroDivisionError


    3.12 Prácticas guiadas

    Práctica 1 — Conversión robusta

    Pide un número y devuélvelo como float. Si hay error, devuelve None. Usa else para imprimir “OK”.

    def a_float(s: str) -> float | None:
        try:
            v = float(s)
        except ValueError:
            return None
        else:
            print("OK")
            return v
    

    Práctica 2 — Copia de archivo con limpieza

    Lee de origen.txt y escribe en destino.txt. Maneja FileNotFoundError y usa with.

    try:
        with open("origen.txt", "r") as src, open("destino.txt", "w") as dst:
            dst.write(src.read())
    except FileNotFoundError as e:
        print("Falta:", e.filename)
    else:
        print("Copia realizada")
    

    Práctica 3 — Registro y re-lanzamiento

    Envuelve una función que pueda fallar; registra el error y vuelve a lanzarlo para que lo maneje el llamador.

    def envoltorio(fn, *args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except Exception as e:
            print("[LOG]:", type(e).__name__, e)
            raise
    



    Parte 4 — Lanzamiento de excepciones con raise

    Manejar excepciones es la mitad del juego; la otra mitad es lanzarlas cuando detectamos
    condiciones inválidas. Con raise comunicamos de forma explícita que algo no cumple
    las precondiciones o que el flujo debe interrumpirse y ser atendido por el llamador.


    4.1 Sintaxis básica de raise

    # Lanzar una excepción con mensaje
    raise ValueError("El parámetro 'edad' debe ser >= 0")

    Puedes lanzar cualquier instancia de excepción que herede de Exception (o una subclase).

    if edad < 0:
        raise ValueError("Edad no puede ser negativa")

    4.2 ¿Cuándo usar raise?

    • 🔹 Validación de entradas (precondiciones).
    • 🔹 Estados imposibles o inconsistentes (invariantes rotos).
    • 🔹 Operaciones que no pueden continuar con datos corruptos o ausentes.
    • 🔹 En librerías: señalar error al código cliente, en lugar de “tragar” el problema.
    def transferir(origen, destino, importe):
        if importe <= 0:
            raise ValueError("El importe debe ser positivo")
        if origen.saldo < importe:
            raise RuntimeError("Saldo insuficiente")
        origen.saldo -= importe
        destino.saldo += importe

    4.3 Re-lanzamiento de excepciones (propagación)

    Puedes capturar para registrar/limpiar y luego re-lanzar la misma excepción con raise “en vacío”.

    def leer_config(ruta):
        try:
            with open(ruta) as f:
                return f.read()
        except FileNotFoundError as e:
            print("[LOG] Config faltante:", e.filename)
            raise  # Propaga al llamador

    4.4 Encadenamiento de excepciones con from

    Útil cuando quieres traducir una excepción de bajo nivel a otra del dominio, conservando el contexto original.

    class ConfigError(Exception):
        pass
    
    def parsear_config(texto):
        try:
            return int(texto)  # ejemplo artificial
        except ValueError as err:
            raise ConfigError("Formato de configuración inválido") from err

    El traceback mostrará la excepción de alto nivel y la causa original (desarrollo más fácil).


    4.5 Excepciones personalizadas

    Crea tus propias excepciones para expresar semántica de dominio. Hereda de Exception o de una subclase adecuada.

    class AppError(Exception):
        """Error base de la aplicación."""
    
    class ValidacionError(AppError):
        """Datos de entrada inválidos."""
    
    class PermisoDenegado(AppError):
        """Operación no autorizada."""
    
    def crear_usuario(nombre: str, edad: int):
        if not nombre:
            raise ValidacionError("El nombre es obligatorio")
        if edad < 0:
            raise ValidacionError("La edad no puede ser negativa")
        # ...
        return {"nombre": nombre, "edad": edad}
    💡 Tip de diseño: define un árbol corto de excepciones y documenta en qué casos se lanza cada una.

    4.6 Mensajes claros y datos útiles

    Redacta mensajes precisos: qué falló, por qué y (si procede) cómo resolverlo.
    Añade contexto (valores, rutas, ids) sin filtrar datos sensibles.

    def establecer_iva(porcentaje: float):
        if not 0 <= porcentaje <= 50:
            raise ValueError(f"IVA fuera de rango: {porcentaje}. Debe estar entre 0 y 50")

    4.7 Patrones habituales de validación

    SituaciónExcepción sugeridaEjemplo
    Entrada con tipo correcto pero valor inválidoValueErrorraise ValueError("edad >= 0")
    Operación en estado inadecuadoRuntimeErrorraise RuntimeError("no inicializado")
    Acceso no autorizadoPermissionError o personalizadaraise PermissionError("requiere rol admin")
    Recurso ausenteFileNotFoundError / personalizadaraise FileNotFoundError(ruta)

    4.8 Anti-patrones (evita esto)

    • ❌ Lanzar Exception genérica sin semántica (difícil de manejar aguas arriba).
    • ❌ Usar excepciones para lógica “normal” (control de flujo que no es excepcional).
    • ❌ Mensajes vagos: “error”, “falló algo” (no ayudan a nadie).
    • ❌ Ocultar la causa al traducir errores (no usar from).

    4.9 Patrón “validar y continuar”

    def procesar_pedido(pedido: dict) -> None:
        requerido = ("id", "cliente", "importe")
        faltantes = [k for k in requerido if k not in pedido]
        if faltantes:
            raise KeyError(f"Pedido incompleto. Faltan: {', '.join(faltantes)}")
        if pedido["importe"] <= 0:
            raise ValueError("El importe debe ser positivo")
        # OK: continuar con el flujo principal
        print("Pedido procesado:", pedido["id"])

    4.10 Integración con try/except y re-intentos

    import time
    
    class TransientError(Exception):
        pass
    
    def llamar_servicio():
        # simulación de fallo intermitente
        raise TransientError("Timeout en servicio X")
    
    def con_reintentos(fn, intentos=3, espera=0.2):
        for i in range(1, intentos + 1):
            try:
                return fn()
            except TransientError as e:
                print(f"Reintento {i}/{intentos}: {e}")
                time.sleep(espera)
        raise RuntimeError("Servicio no disponible tras reintentos")

    4.11 Excepciones personalizadas con datos extra

    class ApiError(Exception):
        def __init__(self, status: int, mensaje: str, *, endpoint: str | None = None):
            super().__init__(f"[{status}] {mensaje}")
            self.status = status
            self.endpoint = endpoint
    
    # Uso:
    # raise ApiError(404, "Recurso no encontrado", endpoint="/v1/items/7")

    Así puedes inspeccionar atributos en el except y decidir la respuesta.


    4.12 Buenas prácticas

    • ✅ Lanza excepciones específicas y documentadas.
    • ✅ Usa mensajes que indiquen el qué, el por qué y (si aplica) el cómo.
    • ✅ Encadena con from al traducir errores de bajo nivel.
    • ✅ Diseña una jerarquía ligera de errores de dominio.
    • ✅ No sustituyas validación por try/except si puedes comprobar barato antes.

    4.13 Mini-quiz

    1. ¿Qué ventaja tiene raise ... from err frente a un raise simple?
    2. ¿Por qué es preferible lanzar una excepción de dominio (p. ej., ValidacionError)?
    3. Rellena:
      try:
          leer_config("cfg.ini")
      except FileNotFoundError as e:
          raise ConfigError("No se pudo cargar") ____ e
      Respuesta

      from


    4.14 Prácticas guiadas

    Práctica 1 — Validar argumentos

    def dividir(a: float, b: float) -> float:
        if b == 0:
            raise ZeroDivisionError("b no puede ser 0")
        return a / b

    Práctica 2 — Jerarquía mínima de dominio

    class TiendaError(Exception): ...
    class StockInsuficiente(TiendaError): ...
    class ProductoInexistente(TiendaError): ...
    
    def vender(stock: dict, sku: str, unidades: int):
        if sku not in stock:
            raise ProductoInexistente(f"SKU {sku} no existe")
        if stock[sku] < unidades:
            raise StockInsuficiente(f"Stock insuficiente para {sku}")
        stock[sku] -= unidades

    Práctica 3 — Traducir error bajo nivel a dominio

    class CSVError(Exception): ...
    
    def cargar_csv(ruta: str):
        try:
            with open(ruta, "r", encoding="utf-8") as f:
                return f.read().splitlines()
        except FileNotFoundError as e:
            raise CSVError(f"No se encuentra el archivo: {ruta}") from e

    Práctica 4 — Reintentos con error transitorio

    def reintentar(fn, veces=3):
        for i in range(veces):
            try:
                return fn()
            except TimeoutError as e:
                if i == veces - 1:
                    raise RuntimeError("Agotados reintentos") from e



    Parte 5 — Jerarquía de Excepciones

    Python organiza todas las excepciones en una jerarquía de clases.
    Esto permite manejar los errores de forma general (capturando la base) o específica (capturando un tipo concreto).
    Entender esta jerarquía te ayuda a escribir bloques except más precisos y elegantes.


    5.1 Concepto de jerarquía

    En Python, todas las excepciones heredan de la clase BaseException.
    Desde ella se derivan dos ramas principales:

    • SystemExit, KeyboardInterrupt y GeneratorExit — usadas internamente por el intérprete.
    • Exception — base de todas las excepciones que usamos habitualmente en nuestros programas.

    Esto significa que si capturas Exception, atrapas la mayoría de los errores de usuario, pero no los que detienen el intérprete.


    5.2 Diagrama simplificado de jerarquía

    BaseException
     ├── SystemExit
     ├── KeyboardInterrupt
     ├── GeneratorExit
     └── Exception
          ├── ArithmeticError
          │    ├── ZeroDivisionError
          │    ├── OverflowError
          │    └── FloatingPointError
          ├── AssertionError
          ├── AttributeError
          ├── EOFError
          ├── ImportError
          │    └── ModuleNotFoundError
          ├── IndexError
          ├── KeyError
          ├── LookupError
          │    ├── IndexError
          │    └── KeyError
          ├── MemoryError
          ├── NameError
          ├── OSError
          │    ├── FileNotFoundError
          │    ├── PermissionError
          │    └── TimeoutError
          ├── ReferenceError
          ├── RuntimeError
          │    └── RecursionError
          ├── SyntaxError
          │    └── IndentationError
          ├── TypeError
          ├── ValueError
          │    └── UnicodeError
          └── Warning
               ├── DeprecationWarning
               ├── UserWarning
               └── SyntaxWarning

    No necesitas memorizar todo el árbol, pero es útil conocer los más frecuentes y su relación de herencia.


    5.3 Ejemplo práctico de herencia

    Como todas las excepciones son clases, puedes comprobar su tipo y jerarquía con issubclass() o isinstance().

    print(issubclass(ZeroDivisionError, ArithmeticError))  # True
    print(issubclass(ArithmeticError, Exception))           # True
    print(issubclass(Exception, BaseException))             # True
    
    try:
        1 / 0
    except ArithmeticError:
        print("Se capturó un error aritmético")

    Salida:

    Se capturó un error aritmético

    Aunque el error fue ZeroDivisionError, Python lo considera parte de la familia ArithmeticError,
    por lo que fue capturado correctamente.


    5.4 Captura genérica vs específica

    Tipo de capturaVentajasDesventajas
    Captura específica
    except FileNotFoundError:
    ✔️ Precisión
    ✔️ Control claro del flujo
    ✔️ Evita ocultar errores no relacionados
    ❌ Requiere conocer las excepciones posibles
    Captura genérica
    except Exception:
    ✔️ Útil para registrar o mostrar mensajes globales❌ Oculta el tipo de error real
    ❌ Dificulta la depuración

    5.5 Captura multinivel (padre e hijo)

    Si defines varios except, el orden importa:
    el bloque se evalúa de arriba abajo. Python ejecutará el primero que coincida.

    try:
        1 / 0
    except ZeroDivisionError:
        print("Error específico: división entre cero")
    except ArithmeticError:
        print("Error aritmético general")

    Salida:

    Error específico: división entre cero

    Si inviertes el orden, el genérico capturaría antes al específico y el segundo bloque nunca se ejecutaría.


    5.6 Herencia en excepciones personalizadas

    Al crear tus propias excepciones, elige una superclase adecuada según la naturaleza del error.

    class AppError(Exception):
        """Error base de la aplicación."""
    
    class EntradaInvalida(AppError, ValueError):
        """Valor fuera de rango o formato incorrecto."""
    
    class ConexionFallida(AppError, ConnectionError):
        """Error al conectar con el servidor."""
    
    try:
        raise ConexionFallida("No se pudo conectar al servidor API.")
    except AppError as e:
        print("Error de aplicación:", type(e).__name__)

    Salida:

    Error de aplicación: ConexionFallida

    5.7 Consejos para diseñar jerarquías personalizadas

    • ✅ Define una excepción base (por ejemplo, AppError o MiProyectoError).
    • ✅ Agrupa subclases por tipo de error lógico (validación, red, base de datos, permisos, etc.).
    • ✅ Mantén una jerarquía simple (2 o 3 niveles como máximo).
    • ✅ Evita heredar directamente de BaseException (usa Exception).
    • ✅ Documenta claramente cuándo lanzar cada tipo.

    5.8 Casos prácticos

    Ejemplo 1 — Captura general vs específica

    try:
        with open("no_existe.txt") as f:
            contenido = f.read()
    except FileNotFoundError:
        print("El archivo no existe.")
    except OSError:
        print("Error general de sistema de archivos.")
    except Exception:
        print("Error desconocido.")

    Salida:

    El archivo no existe.

    Ejemplo 2 — Árbol personalizado de dominio

    class BancoError(Exception): ...
    class CuentaError(BancoError): ...
    class SaldoInsuficiente(CuentaError): ...
    class CuentaNoExiste(CuentaError): ...
    
    def retirar(cuentas, id, cantidad):
        if id not in cuentas:
            raise CuentaNoExiste(f"La cuenta {id} no existe.")
        if cuentas[id] < cantidad:
            raise SaldoInsuficiente("Saldo insuficiente.")
        cuentas[id] -= cantidad
        return cuentas[id]
    
    cuentas = {"A1": 100}
    
    try:
        retirar(cuentas, "A1", 200)
    except BancoError as e:
        print("Error bancario:", type(e).__name__)

    Salida:

    Error bancario: SaldoInsuficiente

    Capturar BancoError te permite interceptar todos los errores de ese dominio,
    sin perder detalle ni tener que listar cada tipo.


    5.9 Inspección y depuración

    Puedes examinar la jerarquía de excepciones directamente desde el intérprete:

    help(Exception)
    help(ArithmeticError)
    help(OSError)

    O imprimir el nombre de clase de una excepción capturada:

    try:
        x = 1 / 0
    except Exception as e:
        print("Tipo de excepción:", type(e).__name__)

    Salida:

    Tipo de excepción: ZeroDivisionError

    5.10 Mini-quiz

    1. ¿Cuál es la clase base de todas las excepciones en Python?
    2. ¿Qué pasa si capturas Exception pero no BaseException?
    3. ¿Por qué es importante el orden de los bloques except?
    💡 Ver respuestas
    • 1️⃣ BaseException.
    • 2️⃣ Capturas los errores normales, pero no los que detienen el intérprete (SystemExit, KeyboardInterrupt…).
    • 3️⃣ Porque el primer bloque coincidente se ejecuta y los demás se ignoran.

    5.11 Prácticas guiadas

    Ejercicio 1 — Captura jerárquica

    try:
        resultado = 10 / 0
    except ArithmeticError:
        print("Error aritmético capturado correctamente")

    Ejercicio 2 — Excepciones de dominio

    class AppError(Exception): ...
    class ValidacionError(AppError): ...
    class ConexionError(AppError): ...
    
    def ejecutar_operacion(x):
        if x < 0:
            raise ValidacionError("Número negativo no permitido")
        if x == 0:
            raise ConexionError("Conexión perdida")
        return x * 10
    
    for valor in [-1, 0, 5]:
        try:
            print(ejecutar_operacion(valor))
        except AppError as e:
            print("Error de aplicación:", type(e).__name__)

    Salida:

    Error de aplicación: ValidacionError  
    Error de aplicación: ConexionError  
    50

    Ejercicio 3 — Verificar jerarquía personalizada

    print(issubclass(ValidacionError, AppError))  # True
    print(issubclass(ConexionError, Exception))   # True
    print(issubclass(AppError, BaseException))    # True



    Parte 6 — Depuración con assert

    La sentencia assert es una herramienta de depuración para verificar
    invariantes y suposiciones internas durante el desarrollo. Si la condición es falsa,
    Python lanza AssertionError (con un mensaje opcional).


    6.1 Sintaxis y comportamiento

    assert condicion, "Mensaje opcional si la aserción falla"
    • Si condicion es True → no pasa nada.
    • Si condicion es FalseAssertionError con el mensaje.
    def porcentaje(p):
        assert 0 <= p <= 100, f"Porcentaje fuera de rango: {p}"
        return p / 100
    
    print(porcentaje(25))     # 0.25
    print(porcentaje(130))    # AssertionError: Porcentaje fuera de rango: 130
    💡 Idea clave: assert verifica supuestos del desarrollador (contratos internos),
    no valida entradas de usuario ni controla flujo en producción.

    6.2 Activación y desactivación (modo optimizado)

    Python elimina las aserciones al ejecutar con optimizaciones (-O o -OO), por lo que
    no deben tener efectos secundarios.

    # Ejecutar con aserciones activas (por defecto)
    python app.py
    
    # Ejecutar desactivando aserciones
    python -O app.py
    • Con -O, el bytecode resultante omite las sentencias assert.
    • No pongas código con efectos (llamadas a funciones, I/O) dentro de assert.
    # ❌ Mal: tiene efectos laterales que desaparecerán con -O
    assert registrar_evento("verificando...") is None

    6.3 Diferencias: assert vs excepciones

    AspectoassertExcepciones (p.ej., raise ValueError)
    PropósitoVerificar supuestos del desarrolladorManejar errores esperables en tiempo de ejecución
    ÁmbitoDesarrollo / pruebasUsuario final / producción
    Estado en -OSe eliminaSigue activo
    MensajesBreves e internosClaros, accionables

    6.4 Patrones útiles con assert

    Verificar tipos y formatos en código interno

    def totalizar(items):
        # items: lista de números (contrato interno)
        assert isinstance(items, list), "items debe ser list"
        assert all(isinstance(x, (int, float)) for x in items), "Elementos numéricos"
        return sum(items)

    Comprobar invariantes tras una operación

    def retirar(cuenta, importe):
        saldo_prev = cuenta.saldo
        cuenta.saldo -= importe
        # El saldo nunca debe crecer tras retirar
        assert cuenta.saldo <= saldo_prev, "Invariante roto: saldo incrementó"

    Pre y postcondiciones simples

    def normalizar(v):
        assert len(v) > 0, "Vector vacío"
        s = sum(v)
        assert s != 0, "No normalizable (suma 0)"
        return [x / s for x in v]

    6.5 Anti-patrones (lo que NO debes hacer)

    • ❌ Usar assert para validar entrada de usuario o datos externos (usa if + raise).
    • ❌ Poner efectos laterales en la condición o el mensaje.
    • ❌ Depender de assert para lógica de negocio.
    • ❌ Mensajes crípticos: “falló”, “mal” (no ayudan a depurar).
    # ✅ Validación de entrada correcta (siempre activa)
    def set_edad(edad: int):
        if edad < 0:
            raise ValueError("edad debe ser ≥ 0")
        # Internamente, puedes reforzar supuestos:
        assert isinstance(edad, int)

    6.6 assert y testing

    En pytest puedes usar assert directamente; pytest mejora el mensaje al fallar.
    En unittest se prefieren métodos como self.assertEqual, self.assertTrue, etc.

    pytest (estilo directo)

    def test_promedio():
        datos = [8, 9, 10]
        prom = sum(datos) / len(datos)
        assert prom == 9

    unittest

    import unittest
    
    class TestOps(unittest.TestCase):
        def test_suma(self):
            self.assertEqual(2 + 2, 4)
    
    if __name__ == "__main__":
        unittest.main()

    6.7 Mensajes efectivos y trazas útiles

    Incluye el dato conflictivo y el contexto mínimo para entender el fallo rápido:

    def porcentaje(valor):
        assert 0 <= valor <= 1, f"valor={valor} fuera de [0,1]"
        return valor * 100

    6.8 __debug__ y aserciones

    La constante __debug__ es True por defecto y pasa a False con -O.
    Úsala si necesitas proteger código de depuración adicional.

    if __debug__:
        # bloque solo en modo no optimizado
        verificar_estado = True

    6.9 Mini-quiz

    1. ¿Qué excepción lanza un assert fallido?
    2. ¿Por qué no debes usar assert para validar datos de usuario?
    3. ¿Qué ocurre con las aserciones al ejecutar python -O?
    💡 Ver respuestas
    • 1️⃣ AssertionError.
    • 2️⃣ Porque pueden desactivarse y no son para control de flujo en producción.
    • 3️⃣ Se eliminan del bytecode (no se evalúan).

    6.10 Prácticas guiadas

    Ejercicio 1 — Invariantes de lista

    def media(valores):
        assert isinstance(valores, list), "valores debe ser list"
        assert len(valores) > 0, "lista vacía"
        assert all(isinstance(x, (int, float)) for x in valores), "elementos no numéricos"
        return sum(valores) / len(valores)

    Ejercicio 2 — Postcondición simple

    def ordenar(nums):
        nums_ordenados = sorted(nums)
        # Postcondición: está no decreciente
        assert all(nums_ordenados[i] <= nums_ordenados[i+1] for i in range(len(nums_ordenados)-1))
        return nums_ordenados

    Ejercicio 3 — Contrato interno + excepción pública

    def dividir(a, b):
        if b == 0:
            raise ZeroDivisionError("b no puede ser 0")  # público/producción
        # refuerzo interno
        assert isinstance(a, (int, float)) and isinstance(b, (int, float)), "tipos inválidos"
        return a / b

    Ejercicio 4 — Sin efectos laterales

    Refactoriza esta línea para evitar efectos en assert:

    # Original (mal)
    assert log(f"longitud={len(data)}") is None and len(data) > 0
    
    # Solución (bien)
    if __debug__:
        log(f"longitud={len(data)}")
    assert len(data) > 0, "data vacía"

    Cierre: usa assert como red de seguridad durante el desarrollo,
    y excepciones explícitas para tratar errores reales de ejecución.



    Parte 7 — Buenas prácticas y ejercicios integradores

    En esta última sección del módulo aprenderás a combinar todo lo visto: detección, captura,
    lanzamiento y validación de errores en un mismo flujo de trabajo.
    Además, repasaremos las mejores prácticas para escribir código robusto, limpio y mantenible.


    7.1 Principios generales

    • Anticipa los errores más comunes (entradas, archivos, red).
    • Captura solo lo necesario; evita los “try/except” que engullen todo.
    • Usa tipos de excepción específicos (ValueError, TypeError, etc.).
    • No uses excepciones como flujo lógico — para eso están los condicionales.
    • Usa finally para liberar recursos.
    • Documenta las excepciones que puede lanzar una función.
    • Combina assert y raise: el primero para desarrollo, el segundo para producción.
    • Encadena errores con from cuando traduces excepciones de bajo nivel.

    7.2 Patrón de manejo completo

    Este patrón agrupa los elementos principales del manejo de errores en Python:
    try, except, else, finally y raise.

    def procesar_dato(valor: str):
        try:
            numero = int(valor)
        except ValueError:
            raise ValueError(f"Entrada inválida: '{valor}' no es un número.")
        else:
            print(f"El número {numero} es válido.")
            return numero
        finally:
            print("Proceso finalizado.")

    Salida:

    El número 5 es válido.  
    Proceso finalizado.

    7.3 Flujo de control profesional

    Un programa bien diseñado separa la validación, el manejo de excepciones
    y la lógica principal para mantener claridad y coherencia.

    def cargar_archivo(ruta: str) -> str:
        """Lee un archivo y devuelve su contenido."""
        if not ruta.endswith(".txt"):
            raise ValueError("Solo se admiten archivos .txt")
        try:
            with open(ruta, "r", encoding="utf-8") as f:
                return f.read()
        except FileNotFoundError:
            raise FileNotFoundError(f"Archivo no encontrado: {ruta}")
        except PermissionError:
            raise PermissionError(f"Permiso denegado al acceder a: {ruta}")
        finally:
            print("[LOG] Intento de lectura finalizado.")

    7.4 Ejemplo integral 1 — Validación, lanzamiento y captura

    class AplicacionError(Exception): ...
    class DatoInvalido(AplicacionError): ...
    class DivisionInvalida(AplicacionError): ...
    
    def dividir_seguro(a, b):
        try:
            if not all(isinstance(x, (int, float)) for x in (a, b)):
                raise DatoInvalido("Solo se admiten números.")
            if b == 0:
                raise DivisionInvalida("No se puede dividir entre cero.")
            resultado = a / b
        except AplicacionError as e:
            print("⚠️ Error de aplicación:", e)
        else:
            print("Resultado:", resultado)
        finally:
            print("Operación completada.")

    Salida esperada:

    ⚠️ Error de aplicación: No se puede dividir entre cero.  
    Operación completada.

    7.5 Ejemplo integral 2 — Captura jerárquica

    class BancoError(Exception): ...
    class CuentaInexistente(BancoError): ...
    class SaldoInsuficiente(BancoError): ...
    
    def retirar(cuentas, id_cuenta, cantidad):
        if id_cuenta not in cuentas:
            raise CuentaInexistente("Cuenta no encontrada.")
        if cuentas[id_cuenta] < cantidad:
            raise SaldoInsuficiente("Saldo insuficiente.")
        cuentas[id_cuenta] -= cantidad
        return cuentas[id_cuenta]
    
    cuentas = {"A1": 500}
    
    try:
        retirar(cuentas, "A1", 800)
    except SaldoInsuficiente as e:
        print("Error específico:", e)
    except BancoError:
        print("Error bancario general.")

    Salida:

    Error específico: Saldo insuficiente.

    7.6 Ejemplo integral 3 — Uso conjunto de assert y raise

    def procesar_lista(datos):
        assert isinstance(datos, list), "datos debe ser una lista"
        if not datos:
            raise ValueError("La lista no puede estar vacía.")
        if not all(isinstance(x, (int, float)) for x in datos):
            raise TypeError("La lista contiene elementos no numéricos.")
        promedio = sum(datos) / len(datos)
        assert promedio >= 0, "El promedio no puede ser negativo (lógica interna)"
        return promedio

    7.7 Ejemplo integral 4 — Registro y encadenamiento

    class ConfigError(Exception): ...
    
    def cargar_config(ruta):
        try:
            with open(ruta) as f:
                return f.read()
        except FileNotFoundError as e:
            print("[LOG] Archivo de configuración faltante:", ruta)
            raise ConfigError("Error al cargar configuración") from e
    
    try:
        cargar_config("conf.ini")
    except ConfigError as e:
        print("⚠️ Error de configuración detectado:", e.__cause__)

    Salida:

    [LOG] Archivo de configuración faltante: conf.ini  
    ⚠️ Error de configuración detectado: [Errno 2] No such file or directory: 'conf.ini'

    7.8 Ejemplo integral 5 — Manejo de errores en flujo de archivos

    def copiar_archivo(origen, destino):
        try:
            with open(origen, "r") as src:
                contenido = src.read()
            with open(destino, "w") as dst:
                dst.write(contenido)
        except FileNotFoundError:
            print(f"Archivo no encontrado: {origen}")
        except PermissionError:
            print(f"Sin permisos para escribir en {destino}")
        else:
            print("Archivo copiado correctamente.")
        finally:
            print("Proceso completado.")

    7.9 Patrón de registro (logging) recomendado

    En lugar de usar solo print(), usa el módulo logging para registrar errores de forma profesional.

    import logging
    
    logging.basicConfig(level=logging.INFO, filename="app.log", filemode="a")
    
    def conectar_servidor():
        try:
            raise TimeoutError("Tiempo de espera agotado.")
        except TimeoutError as e:
            logging.error("Error de conexión: %s", e)
            raise

    📁 Esto guarda los errores en app.log con fecha, hora y nivel de severidad.


    7.10 Buenas prácticas finales

    ✅ Hacer🚫 Evitar
    Usar try/except en operaciones frágiles (I/O, red, parseo)Encerrar todo el programa en un único try
    Crear excepciones personalizadas por dominioLanzar Exception genérica
    Registrar errores con loggingIgnorar las excepciones con pass
    Usar finally o with para limpiar recursosOlvidar cerrar archivos o conexiones
    Combinar assert en desarrollo y raise en producciónUsar assert para validar entradas de usuario

    7.11 Mini-quiz final

    1. ¿Qué bloque se ejecuta siempre, haya o no error?
    2. ¿Qué palabra clave usas para lanzar tus propias excepciones?
    3. ¿Qué ventaja tiene usar logging frente a print()?
    4. ¿Qué instrucción se desactiva en modo optimizado (-O)?
    💡 Ver respuestas
    • 1️⃣ finally.
    • 2️⃣ raise.
    • 3️⃣ Guarda los registros en archivos, con niveles de severidad y marcas de tiempo.
    • 4️⃣ assert.

    7.12 Prácticas guiadas finales

    Ejercicio 1 — Sistema de reservas

    class ReservaError(Exception): ...
    class AsientoNoDisponible(ReservaError): ...
    class PagoError(ReservaError): ...
    
    def reservar(asiento, ocupado, pago_valido):
        if ocupado:
            raise AsientoNoDisponible("El asiento ya está ocupado.")
        if not pago_valido:
            raise PagoError("Error en el pago.")
        print("✅ Reserva completada.")

    Ejercicio 2 — Cálculo con depuración

    def calcular_promedio(notas):
        assert isinstance(notas, list)
        assert len(notas) > 0, "Lista vacía"
        if not all(0 <= n <= 10 for n in notas):
            raise ValueError("Notas fuera de rango (0-10)")
        return sum(notas) / len(notas)

    Ejercicio 3 — Control de acceso

    class AccesoError(Exception): ...
    
    def autenticar(usuario, clave):
        if usuario != "admin" or clave != "1234":
            raise AccesoError("Credenciales inválidas")
        print("Bienvenido, administrador.")
    
    try:
        autenticar("user", "pass")
    except AccesoError as e:
        print("❌", e)
    else:
        print("✅ Autenticación correcta")

    Ejercicio 4 — Registro de errores en archivo

    import logging
    
    logging.basicConfig(filename="errores.log", level=logging.ERROR)
    
    try:
        1 / 0
    except ZeroDivisionError as e:
        logging.error("Fallo en operación: %s", e)
        print("Error registrado en 'errores.log'")

    7.13 Conclusión del módulo 3.2

    Has aprendido a:

    • ✅ Identificar y clasificar los errores comunes en Python.
    • ✅ Usar los bloques try, except, else y finally.
    • ✅ Lanzar y encadenar excepciones con raise y from.
    • ✅ Crear jerarquías de excepciones personalizadas.
    • ✅ Utilizar assert como herramienta de depuración.
    • ✅ Escribir código limpio, seguro y preparado para el error.
    Nuestra puntuación
    ¡Haz clic para puntuar esta entrada!
    (Votos: 0 Promedio: 0)
    Scroll al inicio