Python & AI Tutorials Logo
Programación Python

26. Técnicas de programación defensiva usando excepciones y validación

La programación defensiva (defensive programming) significa escribir código que anticipa problemas antes de que ocurran. En lugar de asumir que todo funcionará perfectamente, el código defensivo valida entradas, maneja errores con elegancia y comprueba supuestos. Este enfoque crea programas más fiables, más fáciles de depurar(debug) y con menos probabilidades de fallar inesperadamente.

En capítulos anteriores, aprendimos a manejar excepciones cuando ocurren. Ahora aprenderemos cómo prevenir muchos errores desde el principio y cómo detectar problemas temprano cuando sí ocurren.

26.1) Validar argumentos de funciones

Las funciones a menudo reciben datos de otras partes de tu programa o de usuarios. Si una función recibe datos no válidos, podría producir resultados incorrectos, fallar con un error confuso o causar problemas en otras partes de tu programa. La validación de argumentos(argument validation) significa comprobar que los argumentos de la función cumplen tus requisitos antes de usarlos.

26.1.1) ¿Por qué validar argumentos?

Considera esta función que calcula el porcentaje de nota de un estudiante:

python
def calculate_percentage(points_earned, total_points):
    return (points_earned / total_points) * 100
 
# Uso de la función
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%")  # Output: Grade: 85.0%

Esto funciona bien con entradas válidas. Pero, ¿qué pasa con datos problemáticos?

python
# Problema 1: División por cero
percentage = calculate_percentage(85, 0)  # ZeroDivisionError!
 
# Problema 2: Valores negativos (no tiene sentido)
percentage = calculate_percentage(-10, 100)  # -10.0%
 
# Problema 3: points_earned supera total_points (imposible)
percentage = calculate_percentage(120, 100)  # 120.0%

Sin validación, la función o bien falla o produce resultados sin sentido. Los mensajes de error no explican qué salió mal desde la perspectiva de la lógica de negocio: solo muestran fallos técnicos.

26.1.2) Validación básica de argumentos con condicionales

El enfoque más simple de validación usa sentencias if para comprobar los argumentos y lanzar excepciones cuando no son válidos:

python
def calculate_percentage(points_earned, total_points):
    # Validar total_points
    if total_points <= 0:
        raise ValueError("total_points must be positive")
    
    # Validar points_earned
    if points_earned < 0:
        raise ValueError("points_earned cannot be negative")
    
    if points_earned > total_points:
        raise ValueError("points_earned cannot exceed total_points")
    
    # Todas las validaciones pasaron: es seguro calcular
    return (points_earned / total_points) * 100
 
# Uso válido
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%")  # Output: Grade: 85.0%
 
# Uso no válido: mensajes de error claros
try:
    percentage = calculate_percentage(85, 0)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: total_points must be positive
 
try:
    percentage = calculate_percentage(-10, 100)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: points_earned cannot be negative
 
try:
    percentage = calculate_percentage(120, 100)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: points_earned cannot exceed total_points

Ahora, cuando algo sale mal, el mensaje de error explica claramente cuál es el problema y cómo arreglarlo.

26.1.3) Validar tipos de argumentos

A veces necesitas asegurarte de que los argumentos sean del tipo correcto:

python
def calculate_discount(price, discount_percent):
    # Validar tipos
    if not isinstance(price, (int, float)):
        raise TypeError("price must be a number")
    
    if not isinstance(discount_percent, (int, float)):
        raise TypeError("discount_percent must be a number")
    
    # Validar valores
    if price < 0:
        raise ValueError("price cannot be negative")
    
    if not (0 <= discount_percent <= 100):
        raise ValueError("discount_percent must be between 0 and 100")
    
    # Calcular descuento
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount
 
# Uso válido
final_price = calculate_discount(50.00, 20)
print(f"Final price: ${final_price:.2f}")  # Output: Final price: $40.00
 
# Error de tipo
try:
    final_price = calculate_discount("50", 20)
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: price must be a number
 
# Error de valor
try:
    final_price = calculate_discount(50.00, 150)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: discount_percent must be between 0 and 100

La función isinstance() comprueba si un objeto es una instancia de un tipo o tipos especificados. Pasamos una tupla (int, float) para aceptar enteros o floats, ya que ambos son tipos numéricos válidos para precios.

Cuándo validar tipos: la filosofía de Python es el “duck typing”: si un objeto se comporta como lo que necesitas, úsalo. La validación de tipos es más útil cuando:

  • Estás escribiendo una función que usarán otras personas
  • Los errores de tipo causarían fallos confusos más adelante
  • La función forma parte de una API pública o de una biblioteca(library)

26.1.4) Validar argumentos de colecciones

Cuando las funciones aceptan listas y diccionarios u otras colecciones, valida tanto la colección como su contenido:

python
def calculate_average_grade(grades):
    # Validar la colección en sí
    if not isinstance(grades, list):
        raise TypeError("grades must be a list")
    
    if len(grades) == 0:
        raise ValueError("grades list cannot be empty")
    
    # Validar cada nota en la colección
    for i, grade in enumerate(grades):
        if not isinstance(grade, (int, float)):
            raise TypeError(f"grade at index {i} must be a number, got {type(grade).__name__}")
        
        if not (0 <= grade <= 100):
            raise ValueError(f"grade at index {i} must be between 0 and 100, got {grade}")
    
    # Todas las validaciones pasaron
    return sum(grades) / len(grades)
 
# Uso válido
grades = [85, 92, 78, 95]
average = calculate_average_grade(grades)
print(f"Average: {average:.1f}")  # Output: Average: 87.5
 
# Error de lista vacía
try:
    average = calculate_average_grade([])
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: grades list cannot be empty
 
# Tipo de nota no válido
try:
    average = calculate_average_grade([85, "92", 78])
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: grade at index 1 must be a number, got str
 
# Valor de nota no válido
try:
    average = calculate_average_grade([85, 92, 150])
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: grade at index 2 must be between 0 and 100, got 150

Fíjate en cómo incluimos el índice en los mensajes de error cuando validamos elementos de una colección. Esto ayuda a identificar exactamente qué elemento es problemático, especialmente en colecciones grandes.

Tipo no válido

Valor no válido

Válido

Función llamada

Validar
argumentos

Lanzar TypeError

Lanzar ValueError

Ejecutar la lógica de la función

Devolver el resultado

El llamador maneja la excepción

26.2) Comprobar la validez de la entrada del usuario

La entrada del usuario es inherentemente poco fiable: los usuarios cometen errores de escritura, malinterpretan instrucciones o introducen datos en formatos inesperados. Validar la entrada del usuario evita que estos errores provoquen fallos del programa o resultados incorrectos.

26.2.1) Patrón básico de validación de entrada

El patrón fundamental para la validación de entrada combina input() con comprobaciones de validación:

python
# Obtener la entrada del usuario
age_str = input("Enter your age: ")
 
# Validar la entrada
try:
    age = int(age_str)
    if age < 0:
        print("Error: Age cannot be negative")
    elif age > 150:
        print("Error: Age seems unrealistic")
    else:
        print(f"You are {age} years old")
except ValueError:
    print("Error: Please enter a valid number")

Este patrón tiene tres partes:

  1. Obtener la entrada como una cadena(string)
  2. Intentar convertirla al tipo necesario
  3. Comprobar si el valor convertido es válido

Veamos esto en acción con distintas entradas:

python
# Ejemplo de entrada válida
# User enters: 25
# Output: You are 25 years old
 
# Tipo no válido
# User enters: twenty-five
# Output: Error: Please enter a valid number
 
# Valor no válido (negativo)
# User enters: -5
# Output: Error: Age cannot be negative
 
# Valor no válido (poco realista)
# User enters: 200
# Output: Error: Age seems unrealistic

26.2.2) Validar rangos y formatos de entrada

Algunas entradas deben estar dentro de rangos específicos o coincidir con formatos concretos:

python
# Validar un mes (1-12)
month_str = input("Enter month (1-12): ")
try:
    month = int(month_str)
    if not (1 <= month <= 12):
        print("Error: Month must be between 1 and 12")
    else:
        print(f"Month: {month}")
except ValueError:
    print("Error: Please enter a whole number")
 
# Validar formato de email (comprobación simple)
email = input("Enter email: ")
if '@' not in email or '.' not in email:
    print("Error: Email must contain @ and .")
else:
    print(f"Email: {email}")
 
# Validar entrada sí/no
response = input("Continue? (yes/no): ").lower().strip()
if response not in ['yes', 'no', 'y', 'n']:
    print("Error: Please answer yes or no")
else:
    if response in ['yes', 'y']:
        print("Continuing...")
    else:
        print("Stopping...")

La validación del email aquí es intencionalmente simple: solo comprueba una estructura básica. La validación real de emails es mucho más compleja y normalmente usa expresiones regulares, que aprenderemos en el Capítulo 39.

26.2.3) Proporcionar mensajes de error útiles

Los buenos mensajes de error le dicen al usuario exactamente qué salió mal y cómo solucionarlo:

python
# Mensaje de error malo
password = input("Enter password: ")
if len(password) < 8:
    print("Error: Invalid password")  # ¡No es útil!
 
# Mensaje de error mejor
password = input("Enter password: ")
if len(password) < 8:
    print("Error: Password must be at least 8 characters long")
    print(f"Your password is only {len(password)} characters")
 
# Aún mejor: explica todos los requisitos por adelantado
print("Password requirements:")
print("- At least 8 characters")
print("- Must contain at least one number")
password = input("Enter password: ")
 
# Comprobar longitud
if len(password) < 8:
    print(f"Error: Password too short ({len(password)} characters)")
    print("Password must be at least 8 characters")
# Comprobar dígito
elif not any(char.isdigit() for char in password):
    print("Error: Password must contain at least one number")
else:
    print("Password accepted")

La función any() devuelve True si cualquier elemento en un iterable es verdadero. Aquí, char.isdigit() comprueba si cada carácter es un dígito, y any() nos dice si al menos un carácter pasó la prueba.

Falla la conversión

Éxito en la conversión

Fuera de rango

Formato no válido

Válido

Obtener la entrada del usuario

Intentar conversión de tipo

ValueError:
Formato no válido

Comprobar
restricciones del valor

Error de valor:
Mensaje claro

Error de formato:
Mensaje claro

Usar la entrada

Mostrar error,
explicar el formato esperado

26.3) Combinar input(), bucles y try/except para un manejo robusto de la entrada

Las comprobaciones de validación individuales son útiles, pero no manejan errores persistentes del usuario. Si un usuario introduce datos no válidos, tu programa debería darle otra oportunidad. Combinar bucles (loop) con validación crea un manejo robusto de la entrada que sigue preguntando hasta obtener datos válidos.

26.3.1) El patrón básico del bucle de entrada

El patrón fundamental usa un bucle while que continúa hasta que se recibe una entrada válida:

python
# Seguir preguntando hasta obtener una edad válida
while True:
    age_str = input("Enter your age: ")
    try:
        age = int(age_str)
        if age < 0:
            print("Error: Age cannot be negative. Please try again.")
        elif age > 150:
            print("Error: Age seems unrealistic. Please try again.")
        else:
            # Entrada válida: salir del bucle
            break
    except ValueError:
        print("Error: Please enter a valid number.")
 
print(f"You are {age} years old")

Este patrón tiene varios elementos clave:

  • while True: crea un bucle infinito
  • La validación ocurre dentro del bucle
  • break sale del bucle cuando la entrada es válida
  • Los mensajes de error animan al usuario a intentarlo de nuevo

Veamos cómo maneja esto varias entradas:

python
# Ejemplo de interacción:
# Enter your age: twenty
# Error: Please enter a valid number.
# Enter your age: -5
# Error: Age cannot be negative. Please try again.
# Enter your age: 25
# You are 25 years old

26.3.2) Crear funciones de entrada reutilizables

Cuando necesitas el mismo tipo de entrada validada en varios lugares, crea una función:

python
def get_positive_integer(prompt):
    """Seguir preguntando hasta que el usuario introduzca un entero positivo."""
    while True:
        try:
            value = int(input(prompt))
            if value <= 0:
                print("Error: Please enter a positive number.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid whole number.")
 
def get_number_in_range(prompt, min_value, max_value):
    """Seguir preguntando hasta que el usuario introduzca un número en el rango especificado."""
    while True:
        try:
            value = float(input(prompt))
            if value < min_value or value > max_value:
                print(f"Error: Please enter a number between {min_value} and {max_value}.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid number.")
 
# Uso de las funciones
quantity = get_positive_integer("Enter quantity: ")
print(f"Quantity: {quantity}")
 
grade = get_number_in_range("Enter grade (0-100): ", 0, 100)
print(f"Grade: {grade}")
 
temperature = get_number_in_range("Enter temperature (-50 to 50): ", -50, 50)
print(f"Temperature: {temperature}°C")

Estas funciones encapsulan la lógica de validación, haciendo que tu código principal sea más limpio y legible. También garantizan un comportamiento de validación consistente en todo tu programa.

26.4) Usar aserciones para comprobaciones de invariantes en tiempo de desarrollo

Las aserciones(assertions) son un tipo especial de comprobación usada durante el desarrollo para verificar que los supuestos de tu código son correctos. A diferencia de la validación (que maneja errores esperados de usuarios o de datos externos), las aserciones detectan errores de programación: situaciones que nunca deberían ocurrir si tu código es correcto.

26.4.1) Qué son las aserciones y cuándo usarlas

Una aserción(assertion) es una sentencia que siempre debería ser verdadera en un punto concreto de tu código. Si es falsa, algo está fundamentalmente mal en la lógica de tu programa:

python
def calculate_average(numbers):
    # Esto nunca debería ocurrir si la función se llama correctamente
    assert len(numbers) > 0, "numbers list cannot be empty"
    
    return sum(numbers) / len(numbers)
 
# Uso correcto
grades = [85, 90, 78]
average = calculate_average(grades)
print(f"Average: {average:.1f}")  # Output: Average: 84.3
 
# Uso incorrecto: activa la aserción
empty_list = []
average = calculate_average(empty_list)  # AssertionError: numbers list cannot be empty

Cuando una aserción falla, Python lanza un AssertionError con tu mensaje. Esto detiene inmediatamente el programa y te muestra exactamente dónde se violó tu supuesto.

Diferencia clave:

  • Validación (usando if y raise): para manejar problemas esperados de usuarios o de datos externos
  • Aserciones: para detectar bugs de programación durante el desarrollo
python
# Validación: maneja errores esperados del usuario
def get_positive_number(prompt):
    while True:
        try:
            value = float(input(prompt))
            if value <= 0:
                print("Error: Please enter a positive number.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid number.")
 
# Aserción: detecta errores de programación
def calculate_discount(price, discount_rate):
    # Estas condiciones nunca deberían incumplirse si el programa está escrito correctamente
    assert price >= 0, "price should be non-negative"
    assert 0 <= discount_rate <= 1, "discount_rate should be between 0 and 1"
    
    return price * (1 - discount_rate)

26.4.2) Comprobar precondiciones de funciones

Las aserciones son excelentes para verificar que se cumplen las precondiciones (preconditions) de una función (requisitos que deben ser verdaderos antes de que la función se ejecute):

python
def get_list_element(items, index):
    """Obtener un elemento de una lista en el índice especificado."""
    # Precondiciones
    assert isinstance(items, list), "items must be a list"
    assert isinstance(index, int), "index must be an integer"
    assert 0 <= index < len(items), f"index {index} out of range for list of length {len(items)}"
    
    return items[index]
 
# Uso correcto
numbers = [10, 20, 30, 40]
value = get_list_element(numbers, 2)
print(f"Value: {value}")  # Output: Value: 30
 
# Error de programación: tipo incorrecto
value = get_list_element("not a list", 0)  # AssertionError: items must be a list
 
# Error de programación: índice no válido
value = get_list_element(numbers, 10)  # AssertionError: index 10 out of range for list of length 4

Estas aserciones ayudan a detectar bugs durante el desarrollo. Si accidentalmente pasas el tipo incorrecto o un índice no válido, la aserción te dice inmediatamente qué salió mal.

26.4.3) Comprobar postcondiciones de funciones

Las postcondiciones (postconditions) son condiciones que deben ser verdaderas después de que una función se ejecute. Las aserciones pueden verificar que tu función produjo resultados válidos:

python
def calculate_percentage(part, whole):
    """Calcular qué porcentaje representa 'part' de 'whole'."""
    # Precondiciones
    assert whole > 0, "whole must be positive"
    assert part >= 0, "part must be non-negative"
    
    # Calcular porcentaje
    percentage = (part / whole) * 100
    
    # Postcondición: el resultado debería ser un porcentaje válido
    assert 0 <= percentage <= 100, f"percentage {percentage} is outside valid range"
    
    return percentage
 
# Esto funciona correctamente
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")  # Output: Percentage: 25.0%
 
# Esto revela un error lógico en nuestra función
# (no comprobamos que part <= whole)
percentage = calculate_percentage(150, 100)  # AssertionError: percentage 150.0 is outside valid range

La aserción de postcondición detectó un bug en nuestra función: se nos olvidó validar que part no supere whole. Esto es exactamente para lo que sirven las aserciones: detectar errores de programación.

26.4.4) Las aserciones pueden deshabilitarse

Una característica importante de las aserciones es que pueden deshabilitarse al ejecutar Python con el flag -O ("optimize"):

python
# Este archivo se llama test_assertions.py
def divide(a, b):
    assert b != 0, "divisor cannot be zero"
    return a / b
 
result = divide(10, 2)
print(f"Result: {result}")
 
result = divide(10, 0)  # AssertionError cuando las aserciones están habilitadas

Ejecución normal:

bash
python test_assertions.py
# Output: Result: 5.0
# Then: AssertionError: divisor cannot be zero

Ejecución con optimización:

bash
python -O test_assertions.py
# Output: Result: 5.0
# Then: ZeroDivisionError: division by zero

Por eso las aserciones nunca deberían usarse para validar datos externos: si alguien ejecuta tu programa con -O, se omiten todas las aserciones. Usa aserciones solo para detectar bugs de programación durante el desarrollo y las pruebas.

Condición verdadera

Condición falsa

Ejecución del código

Comprobación de aserción

Continuar la ejecución

Lanzar AssertionError
con mensaje

El programa se detiene
muestra el traceback

El desarrollador corrige el bug

© 2025. Primesoft Co., Ltd.
support@primesoft.ai