11. Condicionales complejos, anidamiento profundo y expresiones difíciles

11.1 Objetivo del tema

Los condicionales son necesarios, pero cuando crecen sin control pueden volver una función difícil de leer, probar y modificar. El problema aparece cuando el lector debe recordar muchas condiciones, negaciones y niveles de indentación para entender qué camino se ejecuta.

En este tema veremos formas prácticas de simplificar condicionales en Python: cláusulas de guarda, condiciones con nombre, funciones de consulta, diccionarios de reglas y reducción de anidamiento.

Objetivo práctico: transformar condicionales difíciles en decisiones claras, con nombres expresivos y pruebas que protejan el comportamiento.

11.2 Cuándo sospechar de un condicional

Un condicional merece revisión cuando:

  • Tiene muchas condiciones conectadas con and y or.
  • Usa varias negaciones, como not, != o condiciones invertidas.
  • Contiene varios niveles de if dentro de otros if.
  • Repite condiciones parecidas en varias funciones.
  • Obliga a leer todo el bloque para entender el caso normal.
  • Mezcla validación, reglas de negocio y acciones externas.

11.3 Ejemplo de anidamiento profundo

Observa esta función:

def obtener_descuento(usuario, compra):
    if usuario["activo"]:
        if usuario["email"] != "":
            if compra["total"] > 10000:
                if usuario["tipo"] == "vip":
                    return 0.20
                else:
                    return 0.10
            else:
                return 0
        else:
            return 0
    else:
        return 0

El comportamiento no es complicado, pero el anidamiento obliga a seguir muchos niveles. El caso importante queda enterrado.

11.4 Usar cláusulas de guarda

Una cláusula de guarda permite salir temprano cuando no se cumplen condiciones básicas. Así el caso principal queda menos indentado.

def obtener_descuento(usuario, compra):
    if not usuario["activo"]:
        return 0
    if usuario["email"] == "":
        return 0
    if compra["total"] <= 10000:
        return 0

    if usuario["tipo"] == "vip":
        return 0.20

    return 0.10

El resultado es el mismo, pero el flujo se lee de arriba hacia abajo: primero se descartan casos que no aplican, luego se resuelve el caso válido.

11.5 Nombrar condiciones complejas

Una expresión larga puede ser correcta y aun así difícil de entender.

if usuario["activo"] and usuario["email"] != "" and compra["total"] > 10000 and not usuario["bloqueado"]:
    aplicar_descuento(compra)

Podemos darle un nombre a la condición:

puede_recibir_descuento = (
    usuario["activo"]
    and usuario["email"] != ""
    and compra["total"] > 10000
    and not usuario["bloqueado"]
)

if puede_recibir_descuento:
    aplicar_descuento(compra)

El nombre reduce el esfuerzo de lectura. Si la regla se usa en más de un lugar, conviene extraer una función.

11.6 Extraer funciones de consulta

Una función de consulta responde una pregunta del dominio y devuelve normalmente True o False.

def puede_recibir_descuento(usuario, compra):
    return (
        usuario["activo"]
        and usuario["email"] != ""
        and compra["total"] > 10000
        and not usuario["bloqueado"]
    )


if puede_recibir_descuento(usuario, compra):
    aplicar_descuento(compra)

La condición queda reutilizable y se puede probar de manera aislada.

11.7 Evitar dobles negaciones

Las negaciones acumuladas aumentan la dificultad de lectura.

if not usuario["inactivo"] and not usuario["bloqueado"]:
    habilitar_compra(usuario)

Si podemos modelar el dato de otra manera, suele ser más claro:

if usuario["activo"] and not usuario["bloqueado"]:
    habilitar_compra(usuario)

La primera versión obliga a leer “no inactivo”. La segunda expresa la idea en positivo.

11.8 Reemplazar cadenas de elif por diccionarios

Cuando una condición solo busca un valor según una clave, un diccionario puede ser más claro que muchos elif.

def obtener_impuesto(pais):
    if pais == "AR":
        return 0.21
    elif pais == "UY":
        return 0.22
    elif pais == "CL":
        return 0.19
    return 0.18

Versión con tabla de reglas:

IMPUESTOS = {
    "AR": 0.21,
    "UY": 0.22,
    "CL": 0.19,
}
IMPUESTO_PREDETERMINADO = 0.18


def obtener_impuesto(pais):
    return IMPUESTOS.get(pais, IMPUESTO_PREDETERMINADO)

11.9 Separar validación de decisión

Mezclar validación con reglas de negocio puede producir condicionales difíciles.

def procesar_compra(usuario, compra):
    if usuario["activo"] and usuario["email"] != "" and compra["productos"] and compra["total"] > 0:
        if compra["total"] > 10000:
            return "compra con descuento"
        return "compra normal"
    return "compra inválida"

Una separación más clara:

def es_compra_valida(usuario, compra):
    return (
        usuario["activo"]
        and usuario["email"] != ""
        and bool(compra["productos"])
        and compra["total"] > 0
    )


def procesar_compra(usuario, compra):
    if not es_compra_valida(usuario, compra):
        return "compra inválida"
    if compra["total"] > 10000:
        return "compra con descuento"
    return "compra normal"

11.10 Aplicación sobre ventas_demo

En ventas_demo, si tienes una función como esta:

def obtener_descuento(cliente):
    if cliente == "vip":
        return 0.15
    if cliente == "regular":
        return 0.05
    return 0

Puede expresarse como una tabla de reglas:

DESCUENTOS = {
    "vip": 0.15,
    "regular": 0.05,
}


def obtener_descuento(cliente):
    return DESCUENTOS.get(cliente, 0)

Esto es especialmente útil cuando la cantidad de casos crece.

11.11 Condiciones con nombres de dominio

Un buen nombre convierte una expresión técnica en una regla comprensible.

def supera_limite_envio_gratis(total):
    return total >= LIMITE_ENVIO_GRATIS


def aplicar_envio(total):
    if supera_limite_envio_gratis(total):
        return total
    return total + COSTO_ENVIO

La función supera_limite_envio_gratis comunica una regla de negocio, no solo una comparación numérica.

11.12 Evitar condiciones con efectos secundarios

Una condición debería ser fácil de leer y, preferentemente, no modificar estado mientras se evalúa.

if usuario.pop("activo", False) and usuario["email"] != "":
    enviar_mensaje(usuario)

El método pop modifica el diccionario. Es mejor separar la modificación de la decisión o evitar modificar el dato en ese punto.

esta_activo = usuario.get("activo", False)
tiene_email = usuario["email"] != ""

if esta_activo and tiene_email:
    enviar_mensaje(usuario)

11.13 Proteger simplificaciones con pruebas

Simplificar condicionales puede cambiar comportamiento si invertimos una condición por error. Antes de modificar, conviene tener pruebas para los caminos principales.

def test_obtener_impuesto_para_argentina():
    assert obtener_impuesto("AR") == 0.21


def test_obtener_impuesto_para_pais_desconocido():
    assert obtener_impuesto("BR") == 0.18

Después de cambiar, ejecuta:

python -m pytest

11.14 Ejercicio guiado

Simplifica esta función usando cláusulas de guarda y condiciones con nombre:

def puede_comprar(usuario, producto):
    if usuario["activo"]:
        if not usuario["bloqueado"]:
            if usuario["edad"] >= 18:
                if producto["stock"] > 0:
                    if producto["precio"] <= usuario["saldo"]:
                        return True
    return False

Una mejora posible:

def puede_comprar(usuario, producto):
    if not usuario["activo"]:
        return False
    if usuario["bloqueado"]:
        return False
    if usuario["edad"] < 18:
        return False
    if producto["stock"] <= 0:
        return False

    tiene_saldo_suficiente = producto["precio"] <= usuario["saldo"]
    return tiene_saldo_suficiente

11.15 Otra mejora posible

También podemos extraer consultas con nombres del dominio:

def es_usuario_habilitado(usuario):
    return (
        usuario["activo"]
        and not usuario["bloqueado"]
        and usuario["edad"] >= 18
    )


def hay_stock(producto):
    return producto["stock"] > 0


def tiene_saldo_suficiente(usuario, producto):
    return producto["precio"] <= usuario["saldo"]


def puede_comprar(usuario, producto):
    return (
        es_usuario_habilitado(usuario)
        and hay_stock(producto)
        and tiene_saldo_suficiente(usuario, producto)
    )

Esta versión es útil si esas preguntas se reutilizan o si queremos probar cada regla por separado.

11.16 Ejercicio propuesto

Mejora este código sin cambiar su comportamiento:

def calcular_beneficio(usuario, compra):
    if usuario["activo"] == True and usuario["bloqueado"] == False:
        if compra["total"] > 5000 and compra["total"] < 20000:
            if usuario["tipo"] == "vip" or usuario["tipo"] == "premium":
                return "beneficio alto"
            else:
                return "beneficio normal"
        else:
            return "sin beneficio"
    return "sin beneficio"

Realiza estas tareas:

  • Elimina comparaciones innecesarias con True y False.
  • Usa cláusulas de guarda.
  • Nombra al menos una condición compleja.
  • Extrae una función si mejora la lectura.
  • Agrega pruebas para los casos principales.

11.17 Lista de verificación

Antes de continuar, verifica que puedes hacer lo siguiente:

  • Reconocer condicionales con demasiado anidamiento.
  • Usar cláusulas de guarda para reducir indentación.
  • Nombrar condiciones complejas con variables o funciones.
  • Reemplazar cadenas simples de elif por diccionarios cuando corresponde.
  • Separar validación de reglas de negocio.
  • Evitar condiciones con efectos secundarios.
  • Proteger cambios de lógica con pruebas.

11.18 Conclusión

En este tema vimos que los condicionales complejos no solo dificultan la lectura: también aumentan el riesgo de errores al modificar reglas. Aplicamos cláusulas de guarda, condiciones con nombre, funciones de consulta y tablas de reglas.

En el próximo tema trabajaremos con código muerto, comentarios engañosos y deuda técnica visible.