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.
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.
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.
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.
main devuelve un código de salida. raise SystemExit(main()) convierte ese número en el estado del proceso.
0 indica ejecución correcta.0 indica error.Esto es útil cuando el script se ejecuta desde terminal, automatizaciones o CI.
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.
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
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.
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())
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
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.
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)
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"
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.
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())
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
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__".
En ventas_demo, realiza estas tareas:
argparse.main devuelva códigos de salida.python -m ruff check src tests
python -m black src tests
python -m pytest
Antes de continuar, verifica que puedes hacer lo siguiente:
main que devuelva código de salida.argparse para entrada de terminal.raise SystemExit(main()) correctamente.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.