25. Calidad en scripts Python: entrada, validación, funciones principales y main

25.1 Objetivo del tema

Un script Python puede empezar como un archivo pequeño, pero si mezcla lectura de argumentos, validación, cálculo, salida por consola y manejo de errores, se vuelve difícil de probar y mantener.

En este tema veremos cómo estructurar scripts con funciones claras, una función main, validación explícita, códigos de salida y separación entre lógica de negocio y entrada/salida.

Objetivo práctico: convertir un script Python desordenado en un programa pequeño, claro y testeable.

25.2 Script problemático

Observa este script:

import sys

precio = float(sys.argv[1])
cantidad = int(sys.argv[2])
cliente = sys.argv[3]

total = precio * cantidad
if cliente == "vip":
    total = total * 0.85

print(total)

Funciona si se llama correctamente, pero tiene varios problemas: ejecuta lógica al importar, no valida cantidad de argumentos, mezcla entrada con cálculo y es incómodo de probar.

25.3 Separar cálculo

Primero extraemos la lógica de negocio a una función pura:

def calcular_total(precio: float, cantidad: int, cliente: str) -> float:
    total = precio * cantidad
    if cliente == "vip":
        total *= 0.85
    return total

Ahora podemos probar el cálculo sin depender de la terminal.

25.4 Agregar main

La función main coordina entrada, llamada a la lógica y salida.

import sys


def calcular_total(precio: float, cantidad: int, cliente: str) -> float:
    total = precio * cantidad
    if cliente == "vip":
        total *= 0.85
    return total


def main() -> int:
    precio = float(sys.argv[1])
    cantidad = int(sys.argv[2])
    cliente = sys.argv[3]
    total = calcular_total(precio, cantidad, cliente)
    print(f"{total:.2f}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

El bloque final evita que el script se ejecute al importarlo desde pruebas u otros módulos.

25.5 Por qué usar raise SystemExit

main devuelve un código de salida. raise SystemExit(main()) convierte ese número en el estado del proceso.

  • 0 indica ejecución correcta.
  • Un valor distinto de 0 indica error.

Esto es útil cuando el script se ejecuta desde terminal, automatizaciones o CI.

25.6 Validar argumentos manualmente

Antes de convertir valores, debemos verificar que existan.

def main() -> int:
    if len(sys.argv) != 4:
        print("Uso: python ventas_script.py PRECIO CANTIDAD CLIENTE")
        return 2

    precio = float(sys.argv[1])
    cantidad = int(sys.argv[2])
    cliente = sys.argv[3]
    total = calcular_total(precio, cantidad, cliente)
    print(f"{total:.2f}")
    return 0

El código 2 suele usarse para errores de uso en comandos de terminal.

25.7 Manejar errores de conversión

Si el usuario escribe un precio no numérico, debemos mostrar un mensaje claro.

def convertir_entrada(precio_texto: str, cantidad_texto: str) -> tuple[float, int]:
    try:
        precio = float(precio_texto)
        cantidad = int(cantidad_texto)
    except ValueError as error:
        raise ValueError("Precio y cantidad deben ser numéricos") from error

    return precio, cantidad

25.8 Usar argparse

Para scripts reales, argparse suele ser mejor que leer sys.argv manualmente.

import argparse


def crear_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Calcula el total de una venta")
    parser.add_argument("precio", type=float)
    parser.add_argument("cantidad", type=int)
    parser.add_argument("cliente")
    return parser

argparse genera ayuda, valida tipos básicos y muestra mensajes de uso.

25.9 Script con argparse

import argparse


def calcular_total(precio: float, cantidad: int, cliente: str) -> float:
    total = precio * cantidad
    if cliente == "vip":
        total *= 0.85
    return total


def crear_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Calcula el total de una venta")
    parser.add_argument("precio", type=float)
    parser.add_argument("cantidad", type=int)
    parser.add_argument("cliente")
    return parser


def main() -> int:
    parser = crear_parser()
    args = parser.parse_args()
    total = calcular_total(args.precio, args.cantidad, args.cliente)
    print(f"{total:.2f}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

25.10 Validación de reglas de negocio

argparse valida tipos, pero las reglas de negocio siguen siendo responsabilidad del programa.

def validar_venta(precio: float, cantidad: int) -> None:
    if precio < 0:
        raise ValueError("El precio no puede ser negativo")
    if cantidad <= 0:
        raise ValueError("La cantidad debe ser positiva")

Luego la función principal puede manejar el error:

def main() -> int:
    parser = crear_parser()
    args = parser.parse_args()
    try:
        validar_venta(args.precio, args.cantidad)
        total = calcular_total(args.precio, args.cantidad, args.cliente)
    except ValueError as error:
        print(f"Error: {error}")
        return 2

    print(f"{total:.2f}")
    return 0

25.11 Evitar lógica al importar

Este patrón es importante:

if __name__ == "__main__":
    raise SystemExit(main())

Sin ese bloque, el código del script podría ejecutarse al importarlo desde una prueba, causando errores o salidas inesperadas.

25.12 Probar funciones internas

Las funciones de cálculo y validación se prueban de forma simple:

import pytest

from ventas_script import calcular_total, validar_venta


def test_calcular_total_cliente_vip():
    assert calcular_total(1000, 2, "vip") == 1700


def test_validar_venta_rechaza_cantidad_cero():
    with pytest.raises(ValueError, match="cantidad"):
        validar_venta(1000, 0)

25.13 Probar main con monkeypatch

Si quieres probar main, puedes modificar argumentos con monkeypatch.

import sys

from ventas_script import main


def test_main_devuelve_cero(monkeypatch, capsys):
    monkeypatch.setattr(
        sys,
        "argv",
        ["ventas_script.py", "1000", "2", "vip"],
    )

    codigo = main()
    salida = capsys.readouterr()

    assert codigo == 0
    assert salida.out == "1700.00\n"

25.14 Usar logging en scripts

Si el script necesita diagnóstico técnico, configura logging en main.

import logging


def main() -> int:
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    logger.info("Iniciando cálculo de venta")
    ...
    return 0

No configures logging dentro de funciones de negocio.

25.15 Aplicación sobre ventas_demo

Podemos crear src/ventas_cli.py como punto de entrada del proyecto:

import argparse

from ventas import calcular_total_venta


def crear_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Calcula una venta simple")
    parser.add_argument("precio", type=float)
    parser.add_argument("cantidad", type=int)
    parser.add_argument("--cliente", default="nuevo")
    parser.add_argument("--pais", default="AR")
    return parser


def main() -> int:
    parser = crear_parser()
    args = parser.parse_args()
    productos = [{"precio": args.precio, "cantidad": args.cantidad}]
    total = calcular_total_venta(productos, args.cliente, args.pais)
    print(f"Total: {total:.2f}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

25.16 Ejecutar el script

Desde la raíz del proyecto:

python src/ventas_cli.py 1000 2 --cliente vip --pais AR

También puedes ver la ayuda:

python src/ventas_cli.py --help

25.17 Ejercicio guiado

Convierte este script en una versión con funciones y main:

import sys

nombre = sys.argv[1]
edad = int(sys.argv[2])
if edad >= 18:
    print(f"{nombre} es mayor de edad")
else:
    print(f"{nombre} es menor de edad")

Debe tener una función para clasificar edad, una función main y el bloque if __name__ == "__main__".

25.18 Ejercicio propuesto

En ventas_demo, realiza estas tareas:

  • Crea un script CLI con argparse.
  • Separa cálculo, validación, parseo de argumentos y salida.
  • Haz que main devuelva códigos de salida.
  • Agrega pruebas para funciones internas.
  • Ejecuta herramientas y pruebas.
python -m ruff check src tests
python -m black src tests
python -m pytest

25.19 Lista de verificación

Antes de continuar, verifica que puedes hacer lo siguiente:

  • Evitar lógica ejecutándose al importar un script.
  • Crear una función main que devuelva código de salida.
  • Usar argparse para entrada de terminal.
  • Separar parseo, validación, cálculo y salida.
  • Probar funciones internas sin ejecutar el script completo.
  • Usar raise SystemExit(main()) correctamente.

25.20 Conclusión

En este tema vimos que un script Python también debe tener diseño. Separar entrada, validación, cálculo y salida permite escribir scripts más claros, reutilizables y fáciles de probar.

En el próximo tema trabajaremos con calidad en módulos de negocio: separar reglas, datos y presentación.