3. Legibilidad: nombres, intención y estructura visual del código

3.1 Objetivo del tema

La legibilidad es una de las primeras señales de calidad. Un código legible permite entender qué hace, por qué lo hace y dónde cambiarlo cuando aparece un nuevo requisito. En Python esto es especialmente importante, porque el lenguaje favorece soluciones expresivas y directas.

En este tema trabajaremos con nombres, intención y estructura visual. Haremos mejoras pequeñas sobre el proyecto ventas_demo creado en el tema anterior, ejecutando las pruebas para comprobar que el comportamiento se mantiene.

Objetivo práctico: mejorar la legibilidad de una función Python mediante nombres expresivos, constantes y una estructura visual más fácil de seguir.

3.2 Legibilidad no es decoración

Un error común es pensar que la legibilidad es un detalle estético. En realidad, afecta directamente el costo de mantenimiento. Cuando un nombre es confuso o una función está mal organizada, cada cambio exige más esfuerzo mental.

Observa esta línea:

t = t + x["precio"] * x["cant"]

La operación es simple, pero la intención no está completa. Sabemos que se suma algo, pero no queda claro qué representa t, qué representa x ni por qué la clave se llama cant.

3.3 Punto de partida

En el tema anterior dejamos esta función en src/ventas.py:

def proc(items, cliente, pais):
    t = 0
    for x in items:
        if x["cant"] > 0:
            t = t + x["precio"] * x["cant"]

    if cliente == "vip":
        t = t - (t * 0.15)
    else:
        if cliente == "regular":
            t = t - (t * 0.05)

    if pais == "AR":
        t = t + (t * 0.21)
    else:
        if pais == "UY":
            t = t + (t * 0.22)
        else:
            t = t + (t * 0.19)

    if t < 10000:
        t = t + 1500

    return round(t, 2)

Antes de mejorarla, ejecuta las pruebas de caracterización:

python -m pytest

Las pruebas deben pasar. Si fallan, corrige primero el proyecto base antes de avanzar.

3.4 Nombres que comunican intención

Los nombres deben decir qué representa algo en el problema, no solo qué tipo de dato tiene. Por ejemplo, items puede ser una lista, pero en nuestro dominio son productos o líneas de venta.

Una primera mejora es cambiar nombres generales por nombres del dominio:

  • proc puede ser calcular_total_venta.
  • items puede ser productos.
  • x puede ser producto.
  • t puede ser total o subtotal según el momento del cálculo.
  • cant puede ser cantidad.
Un buen nombre reduce la necesidad de comentarios. Si el nombre explica la intención, el lector avanza sin detenerse a descifrar abreviaturas.

3.5 Cambiar el nombre de la función sin romper pruebas

Si cambiamos proc por calcular_total_venta, las pruebas anteriores dejarán de importar el nombre viejo. Durante una transición podemos conservar un alias temporal.

def calcular_total_venta(productos, cliente, pais):
    total = 0
    for producto in productos:
        if producto["cant"] > 0:
            total = total + producto["precio"] * producto["cant"]

    if cliente == "vip":
        total = total - (total * 0.15)
    else:
        if cliente == "regular":
            total = total - (total * 0.05)

    if pais == "AR":
        total = total + (total * 0.21)
    else:
        if pais == "UY":
            total = total + (total * 0.22)
        else:
            total = total + (total * 0.19)

    if total < 10000:
        total = total + 1500

    return round(total, 2)


proc = calcular_total_venta

El alias proc permite que las pruebas viejas sigan pasando. Más adelante podemos actualizar las pruebas y eliminar el alias.

3.6 Ejecutar pruebas después de cada mejora

Después del cambio anterior, ejecuta:

python -m pytest

La mejora es pequeña, pero conviene comprobarla. Este hábito permite avanzar con seguridad: cambiar un aspecto, verificar, continuar.

Si una prueba falla, el cambio no fue solamente de legibilidad. En ese caso debemos revisar si modificamos el comportamiento por accidente.

3.7 Evitar abreviaturas innecesarias

Las abreviaturas pueden ahorrar algunos caracteres, pero muchas veces agregan ambigüedad. En nuestro ejemplo, cant se entiende con esfuerzo, pero cantidad es directo.

Podemos cambiar los datos de prueba y el código para usar cantidad. Primero actualiza las pruebas:

items = [
    {"precio": 3000, "cantidad": 2},
    {"precio": 1500, "cantidad": 1},
]

Luego cambia el acceso en la función:

for producto in productos:
    if producto["cantidad"] > 0:
        total = total + producto["precio"] * producto["cantidad"]

Cuando los datos pertenecen a nuestro propio proyecto, conviene preferir nombres completos y claros. Si vienen de una API externa, a veces debemos adaptar o traducir esos nombres en una capa separada.

3.8 Nombrar valores importantes

Los números sueltos dificultan entender reglas de negocio. No sabemos si 0.21 es IVA, comisión, recargo o cualquier otra cosa. Para mejorar la legibilidad podemos crear constantes.

DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
IMPUESTO_ARGENTINA = 0.21
IMPUESTO_URUGUAY = 0.22
IMPUESTO_OTRO_PAIS = 0.19
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500

Con constantes, el lector entiende el significado de cada valor sin buscar documentación externa.

3.9 Mejorar la estructura visual

La estructura visual permite distinguir bloques de lógica. En una función con varios pasos, podemos separar mentalmente las etapas con líneas en blanco y nombres coherentes.

def calcular_total_venta(productos, cliente, pais):
    subtotal = 0
    for producto in productos:
        if producto["cantidad"] > 0:
            subtotal += producto["precio"] * producto["cantidad"]

    total_con_descuento = subtotal
    if cliente == "vip":
        total_con_descuento -= subtotal * DESCUENTO_VIP
    elif cliente == "regular":
        total_con_descuento -= subtotal * DESCUENTO_REGULAR

    total_con_impuesto = total_con_descuento
    if pais == "AR":
        total_con_impuesto += total_con_descuento * IMPUESTO_ARGENTINA
    elif pais == "UY":
        total_con_impuesto += total_con_descuento * IMPUESTO_URUGUAY
    else:
        total_con_impuesto += total_con_descuento * IMPUESTO_OTRO_PAIS

    total_final = total_con_impuesto
    if total_final < LIMITE_ENVIO_GRATIS:
        total_final += COSTO_ENVIO

    return round(total_final, 2)

Todavía hay oportunidades de mejora, pero esta versión ya es más legible que el punto de partida.

3.10 Eliminar anidamiento que no aporta claridad

El anidamiento profundo hace que el lector mantenga demasiadas condiciones en la cabeza. En Python, muchas veces un elif expresa mejor una serie de alternativas.

Versión más ruidosa:

if pais == "AR":
    total = total + (total * 0.21)
else:
    if pais == "UY":
        total = total + (total * 0.22)
    else:
        total = total + (total * 0.19)

Versión más clara:

if pais == "AR":
    total += total * IMPUESTO_ARGENTINA
elif pais == "UY":
    total += total * IMPUESTO_URUGUAY
else:
    total += total * IMPUESTO_OTRO_PAIS

3.11 Actualizar las pruebas con nombres nuevos

Cuando el código ya tiene un nombre más claro, podemos actualizar las pruebas para importar la función definitiva:

from ventas import calcular_total_venta

Y cambiar los llamados:

assert calcular_total_venta(productos, "vip", "AR") == 9213.75

Después ejecuta:

python -m pytest

Si todas las pruebas pasan, puedes eliminar el alias temporal proc = calcular_total_venta.

3.12 Código completo mejorado

El archivo src/ventas.py puede quedar así al finalizar este tema:

DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
IMPUESTO_ARGENTINA = 0.21
IMPUESTO_URUGUAY = 0.22
IMPUESTO_OTRO_PAIS = 0.19
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500


def calcular_total_venta(productos, cliente, pais):
    subtotal = 0
    for producto in productos:
        if producto["cantidad"] > 0:
            subtotal += producto["precio"] * producto["cantidad"]

    total_con_descuento = subtotal
    if cliente == "vip":
        total_con_descuento -= subtotal * DESCUENTO_VIP
    elif cliente == "regular":
        total_con_descuento -= subtotal * DESCUENTO_REGULAR

    total_con_impuesto = total_con_descuento
    if pais == "AR":
        total_con_impuesto += total_con_descuento * IMPUESTO_ARGENTINA
    elif pais == "UY":
        total_con_impuesto += total_con_descuento * IMPUESTO_URUGUAY
    else:
        total_con_impuesto += total_con_descuento * IMPUESTO_OTRO_PAIS

    total_final = total_con_impuesto
    if total_final < LIMITE_ENVIO_GRATIS:
        total_final += COSTO_ENVIO

    return round(total_final, 2)

3.13 Pruebas actualizadas

El archivo tests/test_ventas.py puede quedar así:

from ventas import calcular_total_venta


def test_calcula_total_para_cliente_vip_en_argentina():
    productos = [
        {"precio": 3000, "cantidad": 2},
        {"precio": 1500, "cantidad": 1},
    ]

    assert calcular_total_venta(productos, "vip", "AR") == 9213.75


def test_agrega_envio_cuando_el_total_es_bajo():
    productos = [
        {"precio": 1000, "cantidad": 1},
    ]

    assert calcular_total_venta(productos, "regular", "UY") == 2659.0


def test_ignora_items_con_cantidad_cero_o_negativa():
    productos = [
        {"precio": 3000, "cantidad": 1},
        {"precio": 9000, "cantidad": 0},
        {"precio": 9000, "cantidad": -2},
    ]

    assert calcular_total_venta(productos, "nuevo", "CL") == 5070.0

3.14 Ejercicio propuesto

Analiza este fragmento y mejora solamente la legibilidad. No cambies la lógica de negocio.

def g(us):
    r = []
    for u in us:
        if u["a"] == 1 and u["e"] != "":
            r.append(u["e"])
    return r

Realiza estas tareas:

  • Cambia el nombre de la función.
  • Cambia los nombres de parámetros y variables locales.
  • Reemplaza claves abreviadas por nombres más claros.
  • Agrega un ejemplo de uso con tres usuarios.
  • Explica por qué tu versión es más legible.

3.15 Una posible solución

Una solución posible es:

def obtener_emails_de_usuarios_activos(usuarios):
    emails = []
    for usuario in usuarios:
        if usuario["activo"] == 1 and usuario["email"] != "":
            emails.append(usuario["email"])
    return emails


usuarios = [
    {"activo": 1, "email": "ana@example.com"},
    {"activo": 0, "email": "luis@example.com"},
    {"activo": 1, "email": ""},
]

print(obtener_emails_de_usuarios_activos(usuarios))

Más adelante podríamos mejorar también el uso de 1 como indicador de usuario activo, pero en este ejercicio nos concentramos en nombres e intención.

3.16 Lista de verificación

Antes de continuar, revisa si puedes aplicar estas ideas:

  • Elegir nombres que expresen intención y dominio.
  • Evitar abreviaturas innecesarias.
  • Nombrar valores importantes mediante constantes.
  • Separar visualmente etapas distintas dentro de una función.
  • Reemplazar anidamientos innecesarios por estructuras más claras.
  • Ejecutar pruebas después de cada cambio de legibilidad.

3.17 Conclusión

En este tema mejoramos la legibilidad sin cambiar el comportamiento del programa. Usamos nombres más expresivos, constantes para valores importantes, menor anidamiento y una estructura visual más fácil de seguir.

En el próximo tema veremos convenciones de estilo en Python y aplicaremos PEP 8 con ejemplos concretos para reforzar la consistencia del código.