Skip to content
Temas
Python
Python Argparse: cómo crear interfaces de línea de comandos correctamente

Python Argparse: cómo crear interfaces de línea de comandos correctamente

Actualizado el

Escribiste un script en Python que procesa datos exactamente como lo necesitas. Luego tu colega te pide usarlo. "Solo cambia el nombre del archivo en la línea 14 y el umbral en la línea 37", le dices. Cambian la línea equivocada. El script falla. Pasas 20 minutos depurando la edición de otra persona sobre tu código que sí funcionaba. Esto pasa cada vez que alguien necesita ajustar un parámetro, y empeora a medida que el script crece. Los valores codificados dentro de los scripts no escalan: ni para equipos, ni para automatización, ni para producción.

El módulo argparse de Python soluciona esto convirtiendo cualquier script en una herramienta de línea de comandos adecuada. Los usuarios pasan argumentos cuando ejecutan el script. El módulo se encarga del análisis, la conversión de tipos, la validación y la generación del mensaje de ayuda. Viene con la biblioteca estándar, así que no hay nada que instalar. Esta guía recorre todas las funciones de argparse que necesitas: desde argumentos posicionales básicos hasta subcomandos y patrones de diseño reales para CLI.

Qué hace argparse y por qué supera a sys.argv

Todo script de Python tiene acceso a sys.argv, una lista cruda de cadenas de la línea de comandos. Puedes analizarla manualmente:

import sys
 
filename = sys.argv[1]
threshold = float(sys.argv[2])
verbose = "--verbose" in sys.argv

Esto funciona para scripts puntuales, pero se desmorona rápido. No hay mensajes de ayuda. No hay validación de tipos. No hay mensajes de error cuando el usuario olvida un argumento. No hay forma correcta de manejar banderas opcionales. La indexación se rompe en cuanto agregas o eliminas un parámetro.

argparse resuelve todos estos problemas con una API declarativa. Defines qué argumentos acepta tu script y argparse hace el resto:

import argparse
 
parser = argparse.ArgumentParser(description="Process data files")
parser.add_argument("filename", help="Input file to process")
parser.add_argument("--threshold", type=float, default=0.5, help="Detection threshold")
parser.add_argument("--verbose", action="store_true", help="Enable detailed output")
args = parser.parse_args()
 
print(f"Processing {args.filename} with threshold {args.threshold}")

Lo que obtienes gratis:

  • --help automático: Ejecuta python script.py --help y los usuarios verán cada argumento, su tipo y su valor por defecto.
  • Conversión de tipos: --threshold 0.8 se convierte automáticamente en un float. Si alguien pasa --threshold abc, argparse muestra un error claro.
  • Validación: Los argumentos obligatorios faltantes producen mensajes de error útiles en lugar de tracebacks crípticos de IndexError.
  • Orden flexible: Los argumentos opcionales pueden aparecer en cualquier orden. --verbose --threshold 0.8 funciona igual que --threshold 0.8 --verbose.

Así se ve la salida de ayuda:

$ python script.py --help
usage: script.py [-h] [--threshold THRESHOLD] [--verbose] filename
 
Process data files
 
positional arguments:
  filename              Input file to process
 
options:
  -h, --help            show this help message and exit
  --threshold THRESHOLD Detection threshold
  --verbose             Enable detailed output

Tu primer script CLI

Construyamos un script completo desde cero. Esta herramienta saluda a un usuario por nombre, con un conteo opcional de cuántas veces repetir el saludo.

#!/usr/bin/env python3
"""greet.py -- A simple greeting CLI tool."""
 
import argparse
 
def main():
    parser = argparse.ArgumentParser(
        prog="greet",
        description="Greet someone by name",
    )
    parser.add_argument("name", help="The person to greet")
    parser.add_argument(
        "-c", "--count",
        type=int,
        default=1,
        help="Number of times to greet (default: 1)",
    )
    parser.add_argument(
        "-u", "--uppercase",
        action="store_true",
        help="Print greeting in uppercase",
    )
 
    args = parser.parse_args()
 
    greeting = f"Hello, {args.name}!"
    if args.uppercase:
        greeting = greeting.upper()
 
    for _ in range(args.count):
        print(greeting)
 
if __name__ == "__main__":
    main()

Prueba desde la terminal:

$ python greet.py Alice
Hello, Alice!
 
$ python greet.py Bob --count 3
Hello, Bob!
Hello, Bob!
Hello, Bob!
 
$ python greet.py Charlie -c 2 -u
HELLO, CHARLIE!
HELLO, CHARLIE!
 
$ python greet.py
usage: greet [-h] [-c COUNT] [-u] name
greet: error: the following arguments are required: name

Observa tres cosas en este script:

  1. La lógica de análisis vive dentro de main(), no al nivel del módulo. Esto hace que el script pueda importarse sin disparar el análisis de argumentos.
  2. La protección if __name__ == "__main__" garantiza que main() solo se ejecute cuando el script se lanza directamente, no cuando se importa.
  3. Las banderas cortas y largas (-c y --count) dan a los usuarios la opción entre brevedad y claridad.

Estos tres patrones aparecen en cualquier script argparse bien escrito. Adóptalos desde el principio.

Argumentos posicionales

Los argumentos posicionales se definen sin guiones. Son obligatorios por defecto y se interpretan según su posición en la línea de comandos.

import argparse
 
parser = argparse.ArgumentParser(description="Copy files")
parser.add_argument("source", help="Source file path")
parser.add_argument("destination", help="Destination file path")
args = parser.parse_args()
 
print(f"Copying {args.source} -> {args.destination}")
$ python copy.py report.csv /backup/report.csv
Copying report.csv -> /backup/report.csv

Múltiples valores posicionales con nargs

El parámetro nargs controla cuántos valores consume un argumento:

import argparse
 
parser = argparse.ArgumentParser(description="Merge files")
 
# One or more files (at least one required)
parser.add_argument("files", nargs="+", help="Files to merge")
 
# Exactly two values
parser.add_argument("--range", nargs=2, type=int, metavar=("START", "END"),
                    help="Row range to extract")
 
args = parser.parse_args()
print(f"Merging: {args.files}")
if args.range:
    print(f"Range: {args.range[0]} to {args.range[1]}")
$ python merge.py data1.csv data2.csv data3.csv --range 10 500
Merging: ['data1.csv', 'data2.csv', 'data3.csv']
Range: 10 to 500

Aquí está la referencia completa de nargs:

nargs valueMeaningResult typeExample
(omitted)Exactly one valueSingle value"input.csv"
N (integer)Exactly N valuesList of N items[10, 100]
"?"Zero or one valueSingle value or default"config.yml" or None
"*"Zero or more valuesList (possibly empty)[] or ["a", "b"]
"+"One or more valuesList (error if empty)["a"] or ["a", "b"]
argparse.REMAINDERAll remaining argsListEverything after this arg

Conversión de tipos en argumentos posicionales

Los argumentos posicionales son cadenas por defecto. Añade type para convertirlos:

import argparse
 
parser = argparse.ArgumentParser(description="Calculate rectangle area")
parser.add_argument("width", type=float, help="Rectangle width")
parser.add_argument("height", type=float, help="Rectangle height")
args = parser.parse_args()
 
area = args.width * args.height
print(f"Area: {area:.2f}")
$ python area.py 3.5 7.2
Area: 25.20
 
$ python area.py three 7.2
usage: area.py [-h] width height
area.py: error: argument width: invalid float value: 'three'

El mensaje de error es automático y le dice al usuario exactamente qué salió mal.

Argumentos opcionales

Los argumentos opcionales comienzan con - (forma corta) o -- (forma larga). Son opcionales por defecto y pueden aparecer en cualquier orden.

import argparse
 
parser = argparse.ArgumentParser(description="Data exporter")
parser.add_argument("-o", "--output", default="output.csv", help="Output file path")
parser.add_argument("-d", "--delimiter", default=",", help="Field delimiter")
parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed progress")
parser.add_argument("-q", "--quiet", action="store_true", help="Suppress all output")
 
args = parser.parse_args()
print(f"Output: {args.output}")
print(f"Delimiter: {repr(args.delimiter)}")
print(f"Verbose: {args.verbose}")
$ python export.py --output results.tsv --delimiter "\t" --verbose
Output: results.tsv
Delimiter: '\\t'
Verbose: True
 
$ python export.py -o results.json -v
Output: results.json
Delimiter: ','
Verbose: True

Valores por defecto

Todo argumento opcional tiene un valor por defecto. Si no defines uno explícitamente, el valor por defecto es None:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--name", default="World")      # Default: "World"
parser.add_argument("--port", type=int, default=8080) # Default: 8080
parser.add_argument("--log-file")                     # Default: None
 
args = parser.parse_args()
print(f"Name: {args.name}, Port: {args.port}, Log: {args.log_file}")

Nota: cuando un nombre de argumento usa guiones (--log-file), argparse lo convierte en guiones bajos para el nombre del atributo: args.log_file.

Las acciones store_true y store_false

Las banderas booleanas no toman un valor. O están presentes o no lo están:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", action="store_true", help="Enable verbose mode")
parser.add_argument("--no-cache", action="store_true", help="Disable caching")
parser.add_argument("--dry-run", action="store_true", help="Preview without executing")
 
args = parser.parse_args()
# --verbose present -> args.verbose is True
# --verbose absent  -> args.verbose is False

La acción count para niveles de verbosidad

Algunas herramientas usan banderas repetidas para niveles de verbosidad (-v, -vv, -vvv):

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", action="count", default=0,
                    help="Increase verbosity (-v, -vv, -vvv)")
args = parser.parse_args()
 
if args.verbose >= 3:
    print("TRACE level: showing everything")
elif args.verbose >= 2:
    print("DEBUG level: detailed output")
elif args.verbose >= 1:
    print("INFO level: progress updates")
else:
    print("Default: errors only")
$ python tool.py -vvv
TRACE level: showing everything

La acción append para argumentos repetidos

Usa action="append" cuando los usuarios deban poder especificar la misma bandera varias veces:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--tag", action="append", default=[],
                    help="Add a tag (can be repeated)")
args = parser.parse_args()
print(f"Tags: {args.tag}")
$ python tool.py -t bug -t urgent -t backend
Tags: ['bug', 'urgent', 'backend']

Type and Choices

Conversión de tipos integrada

El parámetro type acepta cualquier callable que reciba una cadena y devuelva un valor. Puedes usar tipos integrados, pathlib.Path para rutas de archivo o funciones personalizadas:

import argparse
from pathlib import Path
 
parser = argparse.ArgumentParser()
parser.add_argument("--count", type=int, help="Number of items")
parser.add_argument("--rate", type=float, help="Processing rate")
parser.add_argument("--config", type=Path, help="Config file path")
args = parser.parse_args()

Restringir valores con choices

Usa choices para limitar un argumento a valores permitidos específicos:

import argparse
 
parser = argparse.ArgumentParser(description="Deploy application")
parser.add_argument("environment", choices=["dev", "staging", "production"],
                    help="Target environment")
parser.add_argument("--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR"],
                    default="INFO", help="Logging level")
parser.add_argument("--replicas", type=int, choices=range(1, 11),
                    default=1, help="Number of replicas (1-10)")
args = parser.parse_args()
$ python deploy.py testing
usage: deploy.py [-h] {dev,staging,production}
deploy.py: error: argument environment: invalid choice: 'testing'
  (choose from 'dev', 'staging', 'production')

Funciones de tipo personalizadas

Las funciones de tipo personalizadas son una de las características más potentes de argparse. Te permiten validar y transformar la entrada en tiempo de análisis:

import argparse
from datetime import datetime
 
def positive_int(value):
    """Accept only positive integers."""
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError(f"{value} is not a positive integer")
    return ivalue
 
def date_type(value):
    """Parse YYYY-MM-DD date strings."""
    try:
        return datetime.strptime(value, "%Y-%m-%d").date()
    except ValueError:
        raise argparse.ArgumentTypeError(
            f"Invalid date: '{value}'. Expected format: YYYY-MM-DD"
        )
 
def percentage(value):
    """Accept floats between 0 and 100."""
    fvalue = float(value)
    if not 0 <= fvalue <= 100:
        raise argparse.ArgumentTypeError(
            f"{value} is not a valid percentage (0-100)"
        )
    return fvalue
 
parser = argparse.ArgumentParser()
parser.add_argument("--workers", type=positive_int, default=4)
parser.add_argument("--since", type=date_type, help="Start date (YYYY-MM-DD)")
parser.add_argument("--sample", type=percentage, default=100.0,
                    help="Sample percentage (0-100)")
args = parser.parse_args()
$ python tool.py --workers -3
error: argument --workers: -3 is not a positive integer
 
$ python tool.py --since 2026-13-45
error: argument --since: Invalid date: '2026-13-45'. Expected format: YYYY-MM-DD
 
$ python tool.py --sample 150
error: argument --sample: 150 is not a valid percentage (0-100)

Cada error de validación produce un mensaje claro y accionable sin que tengas que escribir lógica if/else en tu código principal.

Argumentos opcionales requeridos

Puedes forzar que un argumento opcional sea obligatorio:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--config", required=True, help="Path to config file")
parser.add_argument("--output", required=True, help="Output directory")
parser.add_argument("--format", default="json", help="Output format")
args = parser.parse_args()
$ python tool.py --format csv
usage: tool.py [-h] --config CONFIG --output OUTPUT [--format FORMAT]
tool.py: error: the following arguments are required: --config, --output

Por qué a veces esto es una señal de mal diseño: Si cada argumento "opcional" es en realidad obligatorio, probablemente quieras argumentos posicionales. Los opcionales requeridos tienen sentido cuando el nombre de la bandera aporta claridad (--config es más claro que una ruta sin etiqueta) o cuando tienes muchos parámetros obligatorios y las banderas con nombre evitan errores de orden.

Un patrón mejor para entradas verdaderamente obligatorias suele ser un argumento posicional:

# Instead of --config (required optional)
parser.add_argument("config", help="Path to config file")
 
# Use required optionals when the name matters
parser.add_argument("--api-key", required=True, help="API authentication key")

Grupos mutuamente excluyentes

A veces los argumentos entran en conflicto entre sí. Un script puede producir salida JSON o CSV, pero no ambas al mismo tiempo. add_mutually_exclusive_group() impone esta restricción:

import argparse
 
parser = argparse.ArgumentParser(description="Export data")
 
# Output format: pick exactly one
format_group = parser.add_mutually_exclusive_group(required=True)
format_group.add_argument("--json", action="store_true", help="Export as JSON")
format_group.add_argument("--csv", action="store_true", help="Export as CSV")
format_group.add_argument("--parquet", action="store_true", help="Export as Parquet")
 
# Verbosity: pick at most one
verbosity = parser.add_mutually_exclusive_group()
verbosity.add_argument("-v", "--verbose", action="store_true", help="Detailed output")
verbosity.add_argument("-q", "--quiet", action="store_true", help="Minimal output")
 
parser.add_argument("input_file", help="Input data file")
args = parser.parse_args()
 
if args.json:
    print(f"Exporting {args.input_file} as JSON")
elif args.csv:
    print(f"Exporting {args.input_file} as CSV")
elif args.parquet:
    print(f"Exporting {args.input_file} as Parquet")
$ python export.py data.csv --json --csv
error: argument --csv: not allowed with argument --json
 
$ python export.py data.csv
error: one of the arguments --json --csv --parquet is required
 
$ python export.py data.csv --parquet -v
Exporting data.csv as Parquet

Una alternativa práctica para seleccionar formato usa choices en lugar de banderas separadas:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--format", choices=["json", "csv", "parquet"],
                    required=True, help="Output format")
args = parser.parse_args()

Ambos enfoques funcionan. Los grupos mutuamente excluyentes son mejores cuando cada opción necesita argumentos adicionales propios. El enfoque choices es mejor cuando las opciones son cadenas simples.

Subcomandos con add_subparsers

Las herramientas CLI del mundo real usan subcomandos. Piensa en git commit, docker build, pip install. Cada subcomando tiene su propio conjunto de argumentos, su propio texto de ayuda y su propia función manejadora. argparse soporta este patrón mediante add_subparsers():

#!/usr/bin/env python3
"""project.py -- A project management CLI tool."""
 
import argparse
 
def handle_init(args):
    """Initialize a new project."""
    print(f"Creating project '{args.name}'")
    print(f"  Template: {args.template}")
    print(f"  Directory: {args.name}/")
 
def handle_build(args):
    """Build the project."""
    mode = "release" if args.optimize else "debug"
    print(f"Building in {mode} mode")
    if args.target:
        print(f"  Target: {args.target}")
 
def handle_test(args):
    """Run tests."""
    print(f"Running tests (coverage={'on' if args.coverage else 'off'})")
    if args.pattern:
        print(f"  Pattern: {args.pattern}")
 
def handle_deploy(args):
    """Deploy the project."""
    print(f"Deploying to {args.environment}")
    if args.dry_run:
        print("  (DRY RUN -- no actual deployment)")
 
def main():
    # Top-level parser
    parser = argparse.ArgumentParser(
        prog="project",
        description="Manage your project lifecycle",
    )
    parser.add_argument("--version", action="version", version="project 1.0.0")
 
    subparsers = parser.add_subparsers(dest="command", required=True,
                                        help="Available commands")
 
    # init subcommand
    init_cmd = subparsers.add_parser("init", help="Create a new project")
    init_cmd.add_argument("name", help="Project name")
    init_cmd.add_argument("--template", default="basic",
                          choices=["basic", "web", "api", "ml"],
                          help="Project template (default: basic)")
    init_cmd.set_defaults(func=handle_init)
 
    # build subcommand
    build_cmd = subparsers.add_parser("build", help="Build the project")
    build_cmd.add_argument("--optimize", action="store_true",
                           help="Enable release optimizations")
    build_cmd.add_argument("--target", help="Build target platform")
    build_cmd.set_defaults(func=handle_build)
 
    # test subcommand
    test_cmd = subparsers.add_parser("test", help="Run tests")
    test_cmd.add_argument("--coverage", action="store_true",
                          help="Generate coverage report")
    test_cmd.add_argument("--pattern", "-p", help="Test name pattern to match")
    test_cmd.set_defaults(func=handle_test)
 
    # deploy subcommand
    deploy_cmd = subparsers.add_parser("deploy", help="Deploy the project")
    deploy_cmd.add_argument("environment",
                            choices=["staging", "production"],
                            help="Target environment")
    deploy_cmd.add_argument("--dry-run", action="store_true",
                            help="Preview without deploying")
    deploy_cmd.set_defaults(func=handle_deploy)
 
    args = parser.parse_args()
    args.func(args)
 
if __name__ == "__main__":
    main()
$ python project.py init myapp --template web
Creating project 'myapp'
  Template: web
  Directory: myapp/
 
$ python project.py build --optimize
Building in release mode
 
$ python project.py test --coverage -p "test_api*"
Running tests (coverage=on)
  Pattern: test_api*
 
$ python project.py deploy production --dry-run
Deploying to production
  (DRY RUN -- no actual deployment)
 
$ python project.py --help
usage: project [-h] [--version] {init,build,test,deploy} ...
 
Manage your project lifecycle
 
positional arguments:
  {init,build,test,deploy}
                        Available commands
    init                Create a new project
    build               Build the project
    test                Run tests
    deploy              Deploy the project
 
$ python project.py deploy --help
usage: project deploy [-h] [--dry-run] {staging,production}
 
positional arguments:
  {staging,production}  Target environment
 
options:
  -h, --help            show this help message and exit
  --dry-run             Preview without deploying

El patrón clave aquí es set_defaults(func=handler). Cada subcomando guarda su función manejadora en el namespace analizado, y la función principal despacha con args.func(args). Este es el enfoque estándar usado en herramientas CLI de producción.

Grupos de argumentos para una mejor ayuda

Cuando tu herramienta tiene muchos argumentos, puedes agruparlos bajo encabezados lógicos:

import argparse
 
parser = argparse.ArgumentParser(description="Data pipeline tool")
 
input_group = parser.add_argument_group("Input options")
input_group.add_argument("--source", required=True, help="Data source path")
input_group.add_argument("--format", choices=["csv", "json", "parquet"], default="csv")
input_group.add_argument("--encoding", default="utf-8", help="File encoding")
 
transform_group = parser.add_argument_group("Transform options")
transform_group.add_argument("--filter", help="Filter expression")
transform_group.add_argument("--sort-by", help="Column to sort by")
transform_group.add_argument("--limit", type=int, help="Max rows to process")
 
output_group = parser.add_argument_group("Output options")
output_group.add_argument("-o", "--output", required=True, help="Output path")
output_group.add_argument("--compress", action="store_true", help="Compress output")
 
args = parser.parse_args()

La salida de --help agrupa los argumentos bajo sus encabezados, haciendo que largas listas de argumentos sean más fáciles de leer. Esto es solo una función de presentación: no cambia cómo se analizan los argumentos.

Ayuda y formato personalizados

El parámetro formatter_class

import argparse
 
parser = argparse.ArgumentParser(
    prog="analyzer",
    description="Analyze datasets and generate reports.\n\n"
                "Supports CSV, JSON, and Parquet input formats.\n"
                "Output can be filtered, sorted, and aggregated.",
    epilog="Examples:\n"
           "  analyzer data.csv --format json --top 10\n"
           "  analyzer data.csv --columns name age --sort-by age\n"
           "  analyzer data.csv --filter 'age > 30' --output results.csv",
    formatter_class=argparse.RawDescriptionHelpFormatter,
)
FormatterWhat it does
HelpFormatterDefault. Wraps text to fit terminal width.
RawDescriptionHelpFormatterPreserves newlines in description and epilog.
RawTextHelpFormatterPreserves newlines everywhere, including argument help.
ArgumentDefaultsHelpFormatterAppends (default: X) to every argument's help text.

metavar y %(default)s

Controla lo que aparece en los mensajes de uso:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--top", type=int, default=10, metavar="N",
                    help="Show top N results (default: %(default)s)")
parser.add_argument("--format", default="table", metavar="FMT",
                    help="Output format (default: %(default)s)")

Esto produce --top N en el texto de uso en lugar de --top TOP, y el texto de ayuda muestra el valor real por defecto.

argparse vs sys.argv vs click vs typer

Python tiene varios enfoques para analizar argumentos de línea de comandos. Así se comparan:

Featuresys.argvargparseclicktyper
Part of stdlibYesYesNo (pip install)No (pip install)
Type conversionManualBuilt-inBuilt-inAutomatic (type hints)
Help generationNoneAutomaticAutomaticAutomatic
SubcommandsManualadd_subparsers()@group.command()app.command()
ValidationManualchoices, custom typeclick.Choice, callbacksValidators
BoilerplateHighMediumLowVery low
API styleImperativeImperativeDecoratorsDecorators + type hints
Tab completionNoneNoneVia pluginBuilt-in
Prompting / colorsManualNoneBuilt-inBuilt-in
TestingManualparse_args([...])CliRunnerCliRunner
Best forThrowaway scriptsStdlib-only projectsComplex CLIsModern Python 3.7+

Aquí está la misma herramienta implementada con cada enfoque:

# --- sys.argv: raw, fragile, no help ---
import sys
 
name = sys.argv[1] if len(sys.argv) > 1 else "World"
count = int(sys.argv[2]) if len(sys.argv) > 2 else 1
for _ in range(count):
    print(f"Hello, {name}!")
# --- argparse: stdlib, explicit argument definitions ---
import argparse
 
parser = argparse.ArgumentParser(description="Greet someone")
parser.add_argument("name", nargs="?", default="World", help="Name to greet")
parser.add_argument("-c", "--count", type=int, default=1, help="Repetitions")
args = parser.parse_args()
for _ in range(args.count):
    print(f"Hello, {args.name}!")
# --- click: decorators, pip install click ---
import click
 
@click.command()
@click.argument("name", default="World")
@click.option("-c", "--count", default=1, type=int, help="Repetitions")
def greet(name, count):
    """Greet someone."""
    for _ in range(count):
        click.echo(f"Hello, {name}!")
 
greet()
# --- typer: type hints, pip install typer ---
import typer
 
def greet(name: str = "World", count: int = 1):
    """Greet someone."""
    for _ in range(count):
        print(f"Hello, {name}!")
 
typer.run(greet)

Cuándo usar cada uno:

  • sys.argv -- Solo para scripts puntuales donde tomas un único valor y no te importa la validación.
  • argparse -- Cuando necesitas una CLI real pero no puedes añadir dependencias externas. Este es el valor predeterminado correcto para la mayoría de los proyectos.
  • click -- Cuando estás construyendo una CLI compleja con grupos de comandos anidados, prompts interactivos, salida con colores y sistemas de plugins.
  • typer -- Cuando quieres el mínimo boilerplate y usas Python 3.7+. Infiera argumentos a partir de type hints y se basa en click.

Proyecto real: CLI procesador de datos CSV

Aquí tienes una herramienta CLI completa y de estilo producción que lee archivos CSV, filtra datos, calcula agregaciones y devuelve resultados en múltiples formatos. Esto demuestra cómo se combinan los patrones de argparse en un proyecto real.

#!/usr/bin/env python3
"""csv_processor.py -- A CLI tool for analyzing CSV data."""
 
import argparse
import csv
import json
import sys
from collections import defaultdict
from pathlib import Path
 
 
def read_csv(filepath, encoding="utf-8"):
    """Read a CSV file and return a list of dictionaries."""
    with open(filepath, "r", encoding=encoding) as f:
        reader = csv.DictReader(f)
        return list(reader)
 
 
def cmd_info(args):
    """Display information about a CSV file."""
    rows = read_csv(args.file, args.encoding)
    columns = list(rows[0].keys()) if rows else []
 
    print(f"File:    {args.file}")
    print(f"Rows:    {len(rows)}")
    print(f"Columns: {len(columns)}")
 
    if args.verbose:
        print("\nColumn details:")
        for col in columns:
            non_empty = sum(1 for r in rows if r[col].strip())
            unique = len(set(r[col] for r in rows))
            print(f"  {col:30s}  non-empty: {non_empty:>6d}  unique: {unique:>6d}")
 
 
def cmd_filter(args):
    """Filter rows where a column matches a value."""
    rows = read_csv(args.file, args.encoding)
 
    if args.column not in rows[0]:
        print(f"Error: column '{args.column}' not found.", file=sys.stderr)
        print(f"Available columns: {', '.join(rows[0].keys())}", file=sys.stderr)
        sys.exit(1)
 
    if args.match == "exact":
        filtered = [r for r in rows if r[args.column] == args.value]
    elif args.match == "contains":
        filtered = [r for r in rows if args.value.lower() in r[args.column].lower()]
    elif args.match == "startswith":
        filtered = [r for r in rows if r[args.column].startswith(args.value)]
    elif args.match == "gt":
        threshold = float(args.value)
        filtered = [r for r in rows if _safe_float(r[args.column], float("-inf")) > threshold]
    elif args.match == "lt":
        threshold = float(args.value)
        filtered = [r for r in rows if _safe_float(r[args.column], float("inf")) < threshold]
 
    print(f"Matched {len(filtered)} of {len(rows)} rows", file=sys.stderr)
    _output_rows(filtered, args)
 
 
def cmd_aggregate(args):
    """Group by a column and compute aggregations."""
    rows = read_csv(args.file, args.encoding)
 
    if args.group_by not in rows[0]:
        print(f"Error: column '{args.group_by}' not found.", file=sys.stderr)
        sys.exit(1)
 
    groups = defaultdict(list)
    for row in rows:
        key = row[args.group_by]
        groups[key].append(row)
 
    results = []
    for key, group_rows in sorted(groups.items()):
        result = {args.group_by: key, "count": len(group_rows)}
 
        if args.sum_column:
            total = sum(_safe_float(r[args.sum_column], 0) for r in group_rows)
            result[f"sum_{args.sum_column}"] = round(total, 2)
 
        if args.avg_column:
            values = [_safe_float(r[args.avg_column], None) for r in group_rows]
            values = [v for v in values if v is not None]
            if values:
                result[f"avg_{args.avg_column}"] = round(sum(values) / len(values), 2)
 
        results.append(result)
 
    if args.sort_by == "count":
        results.sort(key=lambda r: r["count"], reverse=True)
 
    if args.top:
        results = results[:args.top]
 
    _output_rows(results, args)
 
 
def cmd_convert(args):
    """Convert CSV to another format."""
    rows = read_csv(args.file, args.encoding)
 
    if args.columns:
        rows = [{k: r.get(k, "") for k in args.columns} for r in rows]
 
    if args.limit:
        rows = rows[:args.limit]
 
    _output_rows(rows, args)
    count = min(args.limit, len(rows)) if args.limit else len(rows)
    print(f"Converted {count} rows", file=sys.stderr)
 
 
def _safe_float(value, default):
    """Try to convert a string to float, return default on failure."""
    try:
        return float(value)
    except (ValueError, TypeError):
        return default
 
 
def _output_rows(rows, args):
    """Write rows to the specified output in the specified format."""
    if not rows:
        return
 
    output_file = open(args.output, "w", newline="") if args.output else sys.stdout
 
    if args.format == "json":
        json.dump(rows, output_file, indent=2)
        output_file.write("\n")
    elif args.format == "csv":
        writer = csv.DictWriter(output_file, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)
    elif args.format == "tsv":
        writer = csv.DictWriter(output_file, fieldnames=rows[0].keys(),
                                delimiter="\t")
        writer.writeheader()
        writer.writerows(rows)
    elif args.format == "table":
        _print_table(rows, output_file)
 
    if args.output:
        output_file.close()
 
 
def _print_table(rows, out):
    """Print rows as an aligned text table."""
    if not rows:
        return
 
    headers = list(rows[0].keys())
    widths = {h: len(h) for h in headers}
    for row in rows:
        for h in headers:
            widths[h] = max(widths[h], len(str(row.get(h, ""))))
 
    header_line = "  ".join(h.ljust(widths[h]) for h in headers)
    separator = "  ".join("-" * widths[h] for h in headers)
    out.write(header_line + "\n")
    out.write(separator + "\n")
    for row in rows:
        line = "  ".join(str(row.get(h, "")).ljust(widths[h]) for h in headers)
        out.write(line + "\n")
 
 
def main():
    parser = argparse.ArgumentParser(
        prog="csv_processor",
        description="Analyze, filter, aggregate, and convert CSV files.",
        epilog="Examples:\n"
               "  csv_processor info sales.csv --verbose\n"
               "  csv_processor filter sales.csv --column region --value East\n"
               "  csv_processor aggregate sales.csv --group-by region --sum revenue\n"
               "  csv_processor convert sales.csv --format json -o sales.json\n",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--encoding", default="utf-8", help="File encoding")
 
    subparsers = parser.add_subparsers(dest="command", required=True,
                                        help="Available commands")
 
    # --- info ---
    info_p = subparsers.add_parser("info", help="Show CSV file information")
    info_p.add_argument("file", help="CSV file to inspect")
    info_p.add_argument("-v", "--verbose", action="store_true",
                        help="Show per-column statistics")
    info_p.set_defaults(func=cmd_info, format="table", output=None)
 
    # --- filter ---
    filter_p = subparsers.add_parser("filter", help="Filter rows by column value")
    filter_p.add_argument("file", help="Input CSV file")
    filter_p.add_argument("--column", "-c", required=True, help="Column to filter on")
    filter_p.add_argument("--value", "-V", required=True, help="Value to match against")
    filter_p.add_argument("--match", "-m",
                          choices=["exact", "contains", "startswith", "gt", "lt"],
                          default="exact", help="Match mode (default: exact)")
    filter_p.add_argument("--format", "-f",
                          choices=["csv", "json", "tsv", "table"],
                          default="table", help="Output format")
    filter_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    filter_p.set_defaults(func=cmd_filter)
 
    # --- aggregate ---
    agg_p = subparsers.add_parser("aggregate", help="Group and aggregate data")
    agg_p.add_argument("file", help="Input CSV file")
    agg_p.add_argument("--group-by", "-g", required=True, help="Column to group by")
    agg_p.add_argument("--sum", dest="sum_column", help="Column to sum")
    agg_p.add_argument("--avg", dest="avg_column", help="Column to average")
    agg_p.add_argument("--sort-by", choices=["name", "count"], default="name",
                        help="Sort results by name or count")
    agg_p.add_argument("--top", type=int, help="Show only top N groups")
    agg_p.add_argument("--format", "-f",
                        choices=["csv", "json", "tsv", "table"],
                        default="table", help="Output format")
    agg_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    agg_p.set_defaults(func=cmd_aggregate)
 
    # --- convert ---
    conv_p = subparsers.add_parser("convert", help="Convert CSV to another format")
    conv_p.add_argument("file", help="Input CSV file")
    conv_p.add_argument("--format", "-f", required=True,
                        choices=["json", "tsv", "csv", "table"],
                        help="Target format")
    conv_p.add_argument("--columns", nargs="+", metavar="COL",
                        help="Include only these columns")
    conv_p.add_argument("--limit", type=int, help="Max rows to convert")
    conv_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    conv_p.set_defaults(func=cmd_convert)
 
    args = parser.parse_args()
    args.func(args)
 
 
if __name__ == "__main__":
    main()

Esta herramienta combina subcomandos, manejo de tipos personalizado, múltiples formatos de salida, grupos de argumentos y reportes de error correctos. Cada subcomando tiene su propio conjunto de argumentos, y los usuarios obtienen ayuda completa para cada uno.

Visualización de los resultados: Después de filtrar o agregar datos CSV con una herramienta CLI como esta, a menudo querrás explorar la salida visualmente. PyGWalker (opens in a new tab) convierte cualquier DataFrame de pandas en una interfaz interactiva tipo Tableau, directamente en un notebook de Jupyter o en un script. Pasa la salida de tu CLI a un DataFrame y usa PyGWalker para crear gráficos sin escribir código de visualización:

import pandas as pd
import pygwalker as pyg
 
# Load the CSV processor output
df = pd.read_csv("aggregated_results.csv")
 
# Launch interactive visual explorer
walker = pyg.walk(df)

Para el desarrollo interactivo de herramientas CLI como esta, RunCell (opens in a new tab) te permite construir y probar herramientas CLI dentro de Jupyter con asistencia de IA. Puedes prototipar la lógica de análisis de argumentos en celdas del notebook, probar distintas combinaciones de argumentos y luego exportar a un script independiente.

Errores comunes y soluciones

1. error: unrecognized arguments

Esto ocurre cuando pasas un argumento que no fue definido:

$ python tool.py --output results.csv --compress
error: unrecognized arguments: --compress

Solución: Agrega el argumento a tu parser, o usa parse_known_args() si intencionalmente quieres ignorar argumentos desconocidos:

args, unknown = parser.parse_known_args()
# args contains recognized arguments
# unknown is a list of unrecognized strings

2. error: argument --count: invalid int value

$ python tool.py --count three
error: argument --count: invalid int value: 'three'

Solución: Esto es argparse funcionando correctamente. El usuario pasó una cadena no entera a un argumento type=int. Tus funciones de tipo personalizadas deberían lanzar ArgumentTypeError con un mensaje claro.

3. Nombres de atributos con guiones

parser.add_argument("--log-level", default="INFO")
args = parser.parse_args()
 
# This is a syntax error:
# print(args.log-level)  # Python interprets this as args.log minus level
 
# Correct:
print(args.log_level)  # Dashes become underscores

4. El subcomando no dispara ninguna acción

# Problem: no error when no subcommand is given
subparsers = parser.add_subparsers(dest="command")
# Solution: add required=True
subparsers = parser.add_subparsers(dest="command", required=True)

Sin required=True, ejecutar el script sin subcomando no hace nada de forma silenciosa. En Python 3.7+, siempre define required=True en los subparsers.

5. Mensajes de ayuda duplicados por herencia

Si tanto un parser padre como un subparser definen el mismo argumento, los usuarios se confunden:

# BAD: --verbose defined on both parent and child
parser.add_argument("--verbose", action="store_true")
sub = subparsers.add_parser("run")
sub.add_argument("--verbose", action="store_true")  # Shadows parent's --verbose
 
# GOOD: put shared arguments on the parent only
parser.add_argument("--verbose", action="store_true")

6. parse_args() se ejecuta al importar

# BAD: parse_args() fires when another module imports this file
parser = argparse.ArgumentParser()
parser.add_argument("--name")
args = parser.parse_args()  # Crashes if imported without CLI args
 
# GOOD: wrap everything in main()
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--name")
    args = parser.parse_args()
    return args
 
if __name__ == "__main__":
    main()

7. FileType abre archivos demasiado pronto

# Risky: file is opened at parse time, before your code validates other args
parser.add_argument("output", type=argparse.FileType("w"))
 
# Safer: accept a path string, open the file yourself with a context manager
parser.add_argument("output", help="Output file path")
args = parser.parse_args()
with open(args.output, "w") as f:
    f.write("data")

Patrones avanzados

Parsers padre para argumentos compartidos

Cuando varios subcomandos comparten el mismo conjunto de argumentos, usa parsers padre para evitar repetición:

import argparse
 
# Shared arguments
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument("--verbose", "-v", action="store_true")
parent.add_argument("--output", "-o", default="output.csv")
 
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
 
# Both subcommands inherit --verbose and --output
cmd_a = subparsers.add_parser("analyze", parents=[parent])
cmd_a.add_argument("file", help="File to analyze")
 
cmd_b = subparsers.add_parser("compare", parents=[parent])
cmd_b.add_argument("file_a", help="First file")
cmd_b.add_argument("file_b", help="Second file")

Valores por defecto desde variables de entorno

Toma los valores por defecto desde variables de entorno para mayor flexibilidad en despliegues:

import argparse
import os
 
parser = argparse.ArgumentParser()
parser.add_argument("--api-key",
                    default=os.environ.get("API_KEY"),
                    help="API key (or set API_KEY env var)")
parser.add_argument("--port", type=int,
                    default=int(os.environ.get("PORT", "8080")),
                    help="Server port (default: $PORT or 8080)")
args = parser.parse_args()
 
if not args.api_key:
    parser.error("--api-key is required (or set API_KEY environment variable)")

Probar scripts con argparse

Puedes integrar las pruebas de argparse en tus suites de unittest o pytest. Prueba tus herramientas CLI pasando listas de argumentos a parse_args():

import argparse
 
def create_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument("name")
    parser.add_argument("--count", type=int, default=1)
    return parser
 
# In your test file
def test_parser_defaults():
    parser = create_parser()
    args = parser.parse_args(["Alice"])
    assert args.name == "Alice"
    assert args.count == 1
 
def test_parser_with_options():
    parser = create_parser()
    args = parser.parse_args(["Bob", "--count", "5"])
    assert args.name == "Bob"
    assert args.count == 5

Separar create_parser() de main() hace que la definición de argumentos sea testeable sin ejecutar el script completo.

FAQ

¿Para qué se usa Python argparse?

Python argparse es el módulo de la biblioteca estándar para analizar argumentos de línea de comandos. Lee cadenas desde sys.argv, las convierte en objetos tipados de Python, valida entradas, genera la salida de --help y reporta errores cuando los usuarios proporcionan argumentos incorrectos. Se usa para convertir scripts en herramientas CLI adecuadas que aceptan entradas configurables sin editar el código fuente.

¿Cuál es la diferencia entre argumentos posicionales y opcionales en argparse?

Los argumentos posicionales no tienen guiones en su nombre (por ejemplo, parser.add_argument("filename")). Son obligatorios por defecto y se interpretan por posición. Los argumentos opcionales empiezan con guiones (por ejemplo, parser.add_argument("--verbose")). Son opcionales por defecto y pueden aparecer en cualquier orden. Puedes hacer que los opcionales sean obligatorios con required=True.

¿Cómo creo subcomandos como git commit o docker build?

Usa parser.add_subparsers() para crear un grupo de subcomandos. Llama a subparsers.add_parser("name") para cada subcomando. Cada subparser obtiene su propio conjunto de argumentos. Usa set_defaults(func=handler_function) para vincular cada subcomando a una función manejadora y luego llama args.func(args) para despachar.

¿Debería usar argparse o click para mi CLI en Python?

Usa argparse cuando necesites cero dependencias externas y tu CLI sea sencilla. Usa click cuando necesites funciones avanzadas como prompts interactivos, salida colorida en terminal, barras de progreso o grupos de comandos profundamente anidados. Typer es otra opción que usa type hints de Python y requiere incluso menos código repetitivo que click.

¿Cómo valido tipos de entrada personalizados con argparse?

Escribe una función que reciba una cadena y devuelva el valor convertido. Si la entrada es inválida, lanza argparse.ArgumentTypeError con un mensaje descriptivo. Pasa esta función como parámetro type: parser.add_argument("--date", type=my_date_parser). Argparse llama a tu función automáticamente y muestra el mensaje de error si la validación falla.

¿Puedo usar argparse con variables de entorno?

Sí. Establece el parámetro default para leer desde os.environ: parser.add_argument("--api-key", default=os.environ.get("API_KEY")). Esto permite a los usuarios configurar valores mediante variables de entorno y, al mismo tiempo, usar banderas de línea de comandos para sobrescribirlos.

Conclusión

Python argparse transforma scripts de herramientas frágiles, que te obligan a editar el código fuente, en programas de línea de comandos adecuados. El módulo se encarga de la conversión de tipos, validación, generación de ayuda y mensajes de error, para que tú te concentres en lo que hace el script, no en cómo recibe la entrada.

Los patrones que debes recordar:

  • Usa argumentos posicionales para entradas obligatorias y argumentos con guion para los opcionales.
  • Define type para conversión automática, choices para valores restringidos y nargs para múltiples valores.
  • Usa action="store_true" para banderas booleanas y action="count" para niveles de verbosidad.
  • Construye herramientas complejas con add_subparsers() y el patrón de despacho set_defaults(func=handler).
  • Usa grupos mutuamente excluyentes cuando los argumentos entren en conflicto.
  • Encapsula el análisis en main() y protégelo con if __name__ == "__main__".
  • Escribe funciones de tipo personalizadas para validación específica del dominio.

argparse no es la biblioteca de análisis de argumentos más nueva en Python. Pero viene con cada instalación de Python, cubre la gran mayoría de necesidades de CLI y produce herramientas que se comportan como los usuarios esperan. Para la mayoría de los desarrolladores de Python, esa es la mejor opción. Cuando tu herramienta CLI necesite invocar comandos externos, combina argparse con el módulo subprocess para obtener una solución completa de automatización desde línea de comandos.

Guías relacionadas

📚