argparse es el módulo de la biblioteca estándar que convierte argumentos del terminal en valores Python, genera ayuda e informa errores de uso. La respuesta directa es: crea un ArgumentParser, declara argumentos y opciones, llama a parse_args() y envía el resultado a funciones independientes. Utiliza subcommands cuando existan varias acciones y publica un entry point en pyproject.toml para ejecutar la herramienta por su nombre.

En este tutorial construiremos tareas, una CLI que añade y enumera elementos guardados en JSON. Tendrá argumentos posicionales, opciones cortas y largas, subcommands, valores restringidos y salida legible por máquinas. Argparse viene incluido con Python, así que no incorpora dependencias. Trabaja en un entorno virtual con venv para aislar el comando mientras lo desarrollas.

Argumentos posicionales y opciones

Un argumento posicional se identifica por su lugar. Una opción comienza con - o --, suele ser opcional y puede tener un valor predeterminado. Este parser mínimo recibe un archivo y permite seleccionar la salida:

import argparse

parser = argparse.ArgumentParser(description="Resume un archivo de tareas")
parser.add_argument("archivo", help="ruta del archivo JSON")
parser.add_argument("-f", "--formato", choices=["texto", "json"], default="texto")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()

print(args.archivo, args.formato, args.verbose)

choices rechaza valores desconocidos y muestra los permitidos en la ayuda. store_true empieza en False y cambia a True cuando aparece la bandera. Usa type=int o una función personalizada para convertir entradas, recordando que el shell entrega texto inicialmente. La referencia oficial de argparse documenta cada acción y parámetro.

Ejecuta python app.py datos.json --formato json -v y después python app.py --help. Una ayuda útil forma parte de la interfaz. Nombres claros, descripciones breves y defaults visibles evitan que una persona tenga que consultar el código fuente para aprender el uso básico.

Proyecto práctico y estructura

Crea esta estructura:

gestor-tareas/
├── pyproject.toml
├── src/
│   └── tareas_cli/
│       ├── __init__.py
│       └── cli.py
└── tests/
    └── test_cli.py

El código vivirá en src/tareas_cli/cli.py. build_parser construye la interfaz, main interpreta la entrada y las funciones de comando implementan la conducta. Esta separación facilita las pruebas y evita mezclar detalles del parser con el dominio. Consulta módulos y paquetes en Python si no conoces esta organización.

import argparse
import json
from pathlib import Path
from typing import Sequence

PRIORIDADES = ("baja", "media", "alta")

def cargar(ruta: Path) -> list[dict]:
    if not ruta.exists():
        return []
    return json.loads(ruta.read_text(encoding="utf-8"))

def guardar(ruta: Path, tareas: list[dict]) -> None:
    ruta.parent.mkdir(parents=True, exist_ok=True)
    ruta.write_text(
        json.dumps(tareas, ensure_ascii=False, indent=2), encoding="utf-8"
    )

def agregar(args: argparse.Namespace) -> int:
    tareas = cargar(args.archivo)
    tareas.append({"titulo": args.titulo, "prioridad": args.prioridad})
    guardar(args.archivo, tareas)
    print(f"Tarea agregada: {args.titulo}")
    return 0

def listar(args: argparse.Namespace) -> int:
    tareas = cargar(args.archivo)
    seleccionadas = [
        item for item in tareas
        if args.prioridad is None or item["prioridad"] == args.prioridad
    ]
    if args.como_json:
        print(json.dumps(seleccionadas, ensure_ascii=False, indent=2))
    else:
        for indice, item in enumerate(seleccionadas, start=1):
            print(f"{indice}. [{item['prioridad']}] {item['titulo']}")
    return 0

Path evita concatenar rutas manualmente; la guía de pathlib en Python ofrece más ejemplos. Cada comando devuelve un estado entero. Cero comunica éxito, mientras los valores distintos de cero permiten que shells, scripts y procesos CI detecten fallos sin interpretar frases.

Subcommands con parsers específicos

add_subparsers() crea comandos hijos. Cada uno recibe solamente sus argumentos y registra su función mediante set_defaults(func=...):

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="tareas",
        description="Gestiona tareas en un archivo JSON.",
    )
    parser.add_argument(
        "--archivo",
        type=Path,
        default=Path("tareas.json"),
        help="archivo de datos (predeterminado: tareas.json)",
    )
    subparsers = parser.add_subparsers(dest="comando", required=True)

    parser_agregar = subparsers.add_parser("agregar", help="crea una tarea")
    parser_agregar.add_argument("titulo", help="texto de la tarea")
    parser_agregar.add_argument(
        "-p", "--prioridad", choices=PRIORIDADES, default="media"
    )
    parser_agregar.set_defaults(func=agregar)

    parser_listar = subparsers.add_parser("listar", help="muestra tareas")
    parser_listar.add_argument("-p", "--prioridad", choices=PRIORIDADES)
    parser_listar.add_argument("--json", dest="como_json", action="store_true")
    parser_listar.set_defaults(func=listar)
    return parser

def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    return args.func(args)

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

El parámetro opcional argv es intencional. En producción, None indica que argparse debe leer sys.argv; en tests, una lista controla por completo la llamada. El bloque final admite python -m tareas_cli.cli sin ejecutar la interfaz al importar el módulo. El artículo sobre __name__ y __main__ explica este patrón.

Prueba estos comandos:

python -m tareas_cli.cli agregar "Revisar informe" --prioridad alta
python -m tareas_cli.cli listar
python -m tareas_cli.cli --archivo equipo.json listar --json

Las opciones del parser principal normalmente deben aparecer antes del subcommand. Si --archivo también debe funcionar después, decláralo en los parsers hijos o emplea un parser padre compartido. No admitas órdenes alternativas sin necesidad: una gramática previsible es más sencilla de documentar y mantener.

Validación y mensajes de error

Usa choices para conjuntos cerrados y type para conversión. Una función de tipo puede validar una condición y lanzar ArgumentTypeError:

def entero_positivo(valor: str) -> int:
    numero = int(valor)
    if numero < 1:
        raise argparse.ArgumentTypeError("debe ser mayor que cero")
    return numero

No captures el SystemExit generado por errores de sintaxis en la aplicación normal. Proporciona estado 2 y texto de uso coherente. Los problemas operativos, como JSON incorrecto o permisos insuficientes, deben capturarse cerca de main, escribirse en stderr y convertirse en un estado apropiado. La guía de tratamiento de errores con try/except explica límites de excepción precisos.

import sys

def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    try:
        return args.func(args)
    except (OSError, json.JSONDecodeError) as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1

Evita except Exception solo para mostrar un mensaje genérico. Oculta defectos de programación, dificulta el diagnóstico y puede dejar un archivo escrito parcialmente.

Entry point instalable

Un entry point relaciona el comando tareas con una función Python. Un pyproject.toml mínimo es:

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "tareas-cli"
version = "0.1.0"
requires-python = ">=3.10"

[project.scripts]
tareas = "tareas_cli.cli:main"

Instálalo en modo editable con python -m pip install -e . y ejecuta tareas --help. No añadas paréntesis después de main; el instalador genera un wrapper que llama a la función. La especificación de entry points de PyPA define el formato, y el HOWTO oficial de argparse muestra la evolución de una interfaz sencilla. Cuando quieras distribuirla, aprende a publicar un paquete Python en PyPI.

Pruebas de la CLI

Prueba main sin subprocess para obtener rapidez y captura la salida con capsys:

from tareas_cli.cli import main

def test_agregar_y_listar(tmp_path, capsys):
    archivo = tmp_path / "tareas.json"
    assert main(["--archivo", str(archivo), "agregar", "Estudiar"]) == 0
    capsys.readouterr()

    assert main(["--archivo", str(archivo), "listar"]) == 0
    salida = capsys.readouterr().out
    assert "[media] Estudiar" in salida

def test_prioridad_invalida_termina():
    import pytest

    with pytest.raises(SystemExit) as error:
        main(["agregar", "Estudiar", "--prioridad", "urgente"])
    assert error.value.code == 2

Puede existir un test de integración que ejecute el comando instalado, pero la mayoría de las reglas debe permanecer en funciones comunes. La guía de pytest en Python muestra fixtures y parametrización para éxito, sintaxis inválida, almacenamiento corrupto, filtros y JSON.

Compatibilidad, salida y automatización

Una CLI se convierte en una interfaz pública en cuanto otro programa empieza a invocarla. Cambiar el nombre de una opción, modificar un campo de salida o reutilizar un código de retorno puede romper automatizaciones aunque la lógica de negocio siga siendo correcta. Prefiere cambios compatibles y documenta un período de deprecación antes de retirar un comportamiento conocido. Si otros programas consumen el comando, ofrece un modo estable como --format json en lugar de obligarlos a interpretar texto pensado para personas.

Aplica una convención sencilla: los resultados normales van a stdout, los diagnósticos a stderr y cero representa éxito. argparse ya termina con código 2 cuando la sintaxis es inválida; una falla operativa puede devolver 1. Separar ambos flujos permite redirigir los datos sin mezclarlos con advertencias:

tareas listar --format json > tareas.json

Prueba también --help cada vez que cambie el parser. Confirma que cada subcommand explica su finalidad, que las opciones booleanas dejan claro su valor predeterminado y que los ejemplos utilizan una sintaxis realmente aceptada. No pases secretos como argumentos, ya que pueden quedar en el historial del shell o aparecer en la lista de procesos. Obtén tokens y contraseñas desde una fuente adecuada, como una variable de entorno o una entrada segura. Estas decisiones convierten un script útil en una interfaz predecible para terminales, integración continua y tareas programadas.

Preguntas frecuentes

¿Es necesario instalar argparse?

No. Pertenece a la biblioteca estándar de Python. Instala dependencias únicamente para otras capacidades de tu aplicación.

¿Cuándo uso un argumento posicional o una opción?

Usa pocos posicionales para entradas esenciales. Elige opciones para configuración facultativa, banderas y valores con defaults razonables.

¿Los subcommands son mejores que varias flags?

Sí cuando las acciones reciben entradas distintas. agregar y listar resultan más claros que combinaciones conflictivas como --add --list.

¿Un entry point y el bloque __main__ son iguales?

No. El entry point crea un comando después de instalar; el bloque permite ejecutar el módulo directamente. Ambos pueden delegar en la misma main.

Conclusión

Una CLI confiable separa parsing y negocio, ofrece ayuda legible, valida entradas y devuelve estados útiles. Con subcommands, main(argv), pruebas y [project.scripts], este pequeño gestor sirve a personas y automatizaciones. Empieza por la gramática mínima y trata nombres, salida y compatibilidad como partes de una API pública.