Una interfaz de línea de comandos convierte un script en una herramienta que puede controlarse desde la terminal. En lugar de editar el código cada vez que cambia una entrada, el usuario pasa nombres de archivos, formatos, límites, opciones y subcomandos al ejecutar el programa. El módulo estándar argparse interpreta esos valores y genera ayuda y errores automáticamente.
En esta guía aprenderás a crear una CLI en Python con argparse utilizando argumentos posicionales, opciones, banderas booleanas, tipos, valores permitidos, subcomandos, validación, pruebas y empaquetado moderno con pyproject.toml.
Por qué utilizar argparse
Es posible leer sys.argv directamente, pero tendrías que implementar mensajes de ayuda, conversión de tipos, detección de valores ausentes, alias y errores de uso. Argparse reúne esas responsabilidades en una interfaz declarativa.
La documentación oficial de argparse explica acciones, subparsers, formateadores y validación. No necesitas instalar ningún paquete.
Crear una CLI mínima
Guarda este archivo como saludar.py:
import argparse
parser = argparse.ArgumentParser(
description="Muestra un saludo personalizado."
)
parser.add_argument("nombre", help="Nombre de la persona")
args = parser.parse_args()
print(f"¡Hola, {args.nombre}!")Ejecuta:
python saludar.py AnaUsa python saludar.py --help para ver la ayuda generada.
Argumentos posicionales y opcionales
parser.add_argument("archivo")Un argumento posicional se identifica por su lugar. Una opción comienza con una bandera:
parser.add_argument("-o", "--salida")La forma larga mejora la claridad y la corta facilita el uso frecuente.
Convertir tipos
Todos los argumentos llegan inicialmente como texto. Usa type para convertirlos.
parser.add_argument(
"--limite",
type=int,
default=10,
help="Cantidad máxima de resultados",
)Cuando el usuario escribe un valor incompatible, argparse muestra un error y termina con un código distinto de cero.
Rutas con pathlib.Path
from pathlib import Path
parser.add_argument("archivo_entrada", type=Path)Así recibes un objeto Path listo para trabajar con archivos y carpetas.
Banderas booleanas
parser.add_argument(
"-v",
"--detallado",
action="store_true",
help="Muestra información adicional",
)Sin la bandera, el valor es falso. Cuando aparece, es verdadero.
Para crear opciones como --color y --no-color:
parser.add_argument(
"--color",
action=argparse.BooleanOptionalAction,
default=True,
)Restringir valores con choices
parser.add_argument(
"--formato",
choices=["texto", "json", "csv"],
default="texto",
)Los valores aceptados aparecen en la ayuda y cualquier otro se rechaza automáticamente.
Aceptar varios valores
parser.add_argument(
"archivos",
nargs="+",
type=Path,
help="Uno o más archivos",
)?: cero o un valor.*: cero o más.+: uno o más.- Un entero: cantidad exacta.
Repetir una opción
parser.add_argument(
"--etiqueta",
action="append",
default=[],
)Al ejecutar --etiqueta python --etiqueta cli obtienes una lista con dos valores.
Opciones mutuamente excluyentes
grupo = parser.add_mutually_exclusive_group()
grupo.add_argument("--silencioso", action="store_true")
grupo.add_argument("--detallado", action="store_true")Argparse evita que ambas se utilicen al mismo tiempo.
Separar el parser de la lógica
def crear_parser():
parser = argparse.ArgumentParser(
description="Analiza un archivo de texto."
)
parser.add_argument("ruta", type=Path)
parser.add_argument("--codificacion", default="utf-8")
return parser
def main(argv=None):
args = crear_parser().parse_args(argv)
print(args.ruta, args.codificacion)Permitir argv facilita pasar argumentos desde una prueba sin modificar sys.argv.
Validación personalizada
def entero_positivo(valor):
try:
numero = int(valor)
except ValueError as error:
raise argparse.ArgumentTypeError(
f"{valor!r} no es un entero"
) from error
if numero < 1:
raise argparse.ArgumentTypeError(
"el valor debe ser mayor o igual que 1"
)
return numeroparser.add_argument("--limite", type=entero_positivo)Crear subcomandos
parser = argparse.ArgumentParser(prog="tareas")
subparsers = parser.add_subparsers(dest="comando", required=True)
agregar = subparsers.add_parser("agregar")
agregar.add_argument("titulo")
listar = subparsers.add_parser("listar")
listar.add_argument("--completadas", action="store_true")
eliminar = subparsers.add_parser("eliminar")
eliminar.add_argument("id_tarea", type=int)Puedes asociar una función a cada subcomando:
def manejar_agregar(args):
print(f"Agregando: {args.titulo}")
agregar.set_defaults(manejador=manejar_agregar)
args = parser.parse_args()
args.manejador(args)Proyecto completo: estadísticas de archivos
import argparse
import json
import logging
from pathlib import Path
def entero_positivo(valor):
try:
numero = int(valor)
except ValueError as error:
raise argparse.ArgumentTypeError(
f"{valor!r} no es un entero"
) from error
if numero < 1:
raise argparse.ArgumentTypeError(
"el valor debe ser mayor o igual que 1"
)
return numero
def crear_parser():
parser = argparse.ArgumentParser(
prog="textstats",
description="Cuenta líneas, palabras y caracteres.",
)
parser.add_argument(
"rutas",
nargs="+",
type=Path,
help="Archivos o carpetas",
)
parser.add_argument(
"-r",
"--recursivo",
action="store_true",
)
parser.add_argument(
"--patron",
default="*.txt",
)
parser.add_argument(
"--codificacion",
default="utf-8",
)
parser.add_argument(
"--formato",
choices=["texto", "json"],
default="texto",
)
parser.add_argument(
"--limite",
type=entero_positivo,
)
parser.add_argument(
"-v",
"--detallado",
action="store_true",
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0",
)
return parser
def recopilar_archivos(rutas, patron, recursivo):
archivos = []
for ruta in rutas:
if ruta.is_file():
archivos.append(ruta)
continue
if ruta.is_dir():
iterador = ruta.rglob(patron) if recursivo else ruta.glob(patron)
archivos.extend(item for item in iterador if item.is_file())
continue
logging.warning("Ruta no encontrada: %s", ruta)
return sorted(set(archivos))
def analizar_archivo(ruta, codificacion):
texto = ruta.read_text(
encoding=codificacion,
errors="replace",
)
return {
"ruta": str(ruta),
"lineas": len(texto.splitlines()),
"palabras": len(texto.split()),
"caracteres": len(texto),
}
def main(argv=None):
args = crear_parser().parse_args(argv)
logging.basicConfig(
level=logging.INFO if args.detallado else logging.WARNING,
format="%(levelname)s: %(message)s",
)
archivos = recopilar_archivos(
args.rutas,
args.patron,
args.recursivo,
)
if args.limite is not None:
archivos = archivos[: args.limite]
if not archivos:
raise SystemExit("No se encontraron archivos.")
resultados = [
analizar_archivo(ruta, args.codificacion)
for ruta in archivos
]
if args.formato == "json":
print(json.dumps(resultados, indent=2, ensure_ascii=False))
else:
for resultado in resultados:
print(resultado["ruta"])
print(f" líneas: {resultado['lineas']}")
print(f" palabras: {resultado['palabras']}")
print(f" caracteres: {resultado['caracteres']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())Ejemplos:
python textstats.py notas.txt
python textstats.py documentos --recursivo
python textstats.py documentos -r --formato json --limite 20
python textstats.py --helpProbar el parser
from pathlib import Path
from textstats import crear_parser
def test_parser():
args = crear_parser().parse_args(
["notas.txt", "--formato", "json", "--limite", "5"]
)
assert args.rutas == [Path("notas.txt")]
assert args.formato == "json"
assert args.limite == 5Empaquetar la CLI
[project]
name = "textstats-cli"
version = "0.1.0"
description = "Estadísticas de archivos de texto"
requires-python = ">=3.10"
[project.scripts]
textstats = "textstats.cli:main"Después de instalar el paquete, el entorno crea el comando textstats.
Errores frecuentes
- Interpretar argumentos al importar el módulo.
- Usar
type=boolpara banderas. - Colocar toda la lógica justo después de
parse_args(). - No escribir ayuda útil.
- Trabajar con rutas como texto cuando
Pathsería más claro. - Probar solamente argumentos válidos.
Conclusión
Una CLI en Python con argparse obtiene ayuda automática, tipos, opciones, banderas y subcomandos sin añadir dependencias. El diseño más mantenible construye el parser en una función, mantiene la lógica separada y permite pasar argumentos a main(argv=None) durante las pruebas.
Lecturas relacionadas
- Gestiona archivos y carpetas de forma más clara con la guía de pathlib en Python.
- Configura mensajes de diagnóstico para el modo detallado con el módulo logging de Python.
- Comprueba argumentos y códigos de salida con la guía de Pytest para principiantes.






