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.