Python Argparse: crie interfaces de linha de comando do jeito certo
Atualizado em
Você escreveu um script em Python que processa dados exatamente do jeito que você precisa. Então um colega pede para usar. "É só mudar o nome do arquivo na linha 14 e o threshold na linha 37", você diz. Ele muda a linha errada. O script quebra. Você passa 20 minutos depurando a edição de outra pessoa no seu código funcional. Isso acontece toda vez que alguém precisa ajustar um parâmetro, e piora à medida que o script cresce. Valores hardcoded dentro de scripts não escalam -- nem para equipes, nem para automação, nem para produção.
O módulo argparse do Python resolve isso ao transformar qualquer script em uma ferramenta de linha de comando adequada. Os usuários passam argumentos quando executam o script. O módulo cuida do parsing, conversão de tipos, validação e geração da mensagem de ajuda. Ele vem com a biblioteca padrão, então não há nada para instalar. Este guia percorre todos os recursos do argparse que você precisa -- desde argumentos posicionais básicos até subcomandos e padrões de design de CLI do mundo real.
O que o argparse faz e por que ele é melhor que sys.argv
Todo script Python tem acesso a sys.argv, uma lista bruta de strings da linha de comando. Você pode fazer o parsing manualmente:
import sys
filename = sys.argv[1]
threshold = float(sys.argv[2])
verbose = "--verbose" in sys.argvIsso funciona para scripts descartáveis, mas rapidamente se desfaz. Não há mensagens de ajuda. Não há validação de tipos. Não há mensagens de erro quando os usuários esquecem um argumento. Não há como lidar corretamente com flags opcionais. A indexação quebra no momento em que você adiciona ou remove um parâmetro.
argparse resolve todos esses problemas com uma API declarativa. Você define quais argumentos seu script aceita, e o argparse cuida do 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}")O que você ganha de graça:
--helpautomático: Executepython script.py --helpe os usuários veem todos os argumentos, seus tipos e seus valores padrão.- Conversão de tipos:
--threshold 0.8vira automaticamente um float. Se alguém passar--threshold abc, o argparse exibe um erro claro. - Validação: Argumentos obrigatórios ausentes geram mensagens de erro úteis em vez de traceback confuso de
IndexError. - Ordenação flexível: Argumentos opcionais podem aparecer em qualquer ordem.
--verbose --threshold 0.8funciona da mesma forma que--threshold 0.8 --verbose.
Veja como a saída da ajuda se parece:
$ 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 outputSeu primeiro script CLI
Vamos construir um script completo do zero. Esta ferramenta cumprimenta um usuário pelo nome, com uma contagem opcional de quantas vezes repetir a saudação.
#!/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()Teste no 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: nameObserve três coisas sobre este script:
- A lógica de parsing vive dentro de
main(), e não no nível do módulo. Isso torna o script importável sem disparar o parsing de argumentos. - O guard
if __name__ == "__main__"garante quemain()só rode quando o script for executado diretamente, e não quando importado. - Flags curtas e longas (
-ce--count) oferecem aos usuários a escolha entre brevidade e clareza.
Esses três padrões aparecem em todo script argparse bem escrito. Siga-os desde o começo.
Argumentos posicionais
Argumentos posicionais são definidos sem hífens. Eles são obrigatórios por padrão e correspondem à posição na linha de comando.
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.csvMúltiplos valores posicionais com nargs
O parâmetro nargs controla quantos valores um argumento consome:
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 500Aqui está a referência completa de nargs:
| nargs value | Meaning | Result type | Example |
|---|---|---|---|
| (omitted) | Exactly one value | Single value | "input.csv" |
N (integer) | Exactly N values | List of N items | [10, 100] |
"?" | Zero or one value | Single value or default | "config.yml" or None |
"*" | Zero or more values | List (possibly empty) | [] or ["a", "b"] |
"+" | One or more values | List (error if empty) | ["a"] or ["a", "b"] |
argparse.REMAINDER | All remaining args | List | Everything after this arg |
Conversão de tipo em argumentos posicionais
Argumentos posicionais são strings por padrão. Adicione type para convertê-los:
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'A mensagem de erro é automática e diz exatamente o que deu errado.
Argumentos opcionais
Argumentos opcionais começam com - (forma curta) ou -- (forma longa). Eles são opcionais por padrão e podem aparecer em qualquer ordem.
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: TrueValores padrão
Todo argumento opcional tem um valor padrão. Se você não definir um explicitamente, ele será 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}")Observação: quando o nome do argumento usa hífens (--log-file), o argparse converte para underscores no nome do atributo: args.log_file.
As ações store_true e store_false
Flags booleanas não recebem valor. Elas estão presentes ou ausentes:
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 FalseA ação count para níveis de verbosidade
Algumas ferramentas usam flags repetidas para níveis de verbosidade (-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 everythingA ação append para argumentos repetidos
Use action="append" quando os usuários precisarem especificar a mesma flag várias vezes:
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']Tipo e choices
Conversão de tipos embutidos
O parâmetro type aceita qualquer callable que receba uma string e retorne um valor. Você pode usar tipos embutidos, pathlib.Path para caminhos de arquivo, ou funções 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()Restringindo valores com choices
Use choices para limitar um 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')Funções de tipo personalizadas
Funções de tipo personalizadas são um dos recursos mais poderosos do argparse. Elas permitem validar e transformar a entrada no momento do parsing:
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 erro de validação produz uma mensagem clara e acionável sem que você escreva qualquer lógica if/else no código principal.
Argumentos opcionais obrigatórios
Você pode forçar um argumento opcional a ser obrigatório:
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, --outputPor que isso às vezes é um code smell: Se todo argumento "opcional" na verdade é obrigatório, você provavelmente deveria usar argumentos posicionais. Argumentos opcionais obrigatórios fazem sentido quando o nome da flag traz clareza (--config é mais claro do que um caminho solto) ou quando você tem muitos parâmetros obrigatórios e flags nomeadas evitam erros de ordem.
Um padrão melhor para entradas realmente obrigatórias é muitas vezes um 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 exclusivos
Às vezes argumentos entram em conflito entre si. Um script pode produzir saída em JSON ou CSV, mas não ambos ao mesmo tempo. add_mutually_exclusive_group() impõe essa restrição:
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 ParquetUma alternativa prática para seleção de formato usa choices em vez de flags separadas:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--format", choices=["json", "csv", "parquet"],
required=True, help="Output format")
args = parser.parse_args()As duas abordagens funcionam. Grupos mutuamente exclusivos são melhores quando cada opção precisa de seus próprios argumentos adicionais. A abordagem com choices é melhor quando as opções são strings simples.
Subcomandos com add_subparsers
Ferramentas CLI do mundo real usam subcomandos. Pense em git commit, docker build, pip install. Cada subcomando tem seu próprio conjunto de argumentos, seu próprio texto de ajuda e seu próprio handler. O argparse suporta esse padrão por meio de 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 deployingO padrão principal aqui é set_defaults(func=handler). Cada subcomando armazena sua função handler no namespace parseado, e a função principal faz o despacho com args.func(args). Esse é o padrão padrão usado em ferramentas CLI de produção.
Grupos de argumentos para uma ajuda melhor
Quando sua ferramenta tem muitos argumentos, você pode agrupá-los sob cabeçalhos 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()A saída de --help agrupa os argumentos sob seus títulos, tornando longas listas de argumentos mais fáceis de ler. Isso é apenas um recurso de apresentação -- não altera como os argumentos são interpretados.
Ajuda e formatação personalizadas
O 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,
)| Formatter | What it does |
|---|---|
HelpFormatter | Default. Wraps text to fit terminal width. |
RawDescriptionHelpFormatter | Preserves newlines in description and epilog. |
RawTextHelpFormatter | Preserves newlines everywhere, including argument help. |
ArgumentDefaultsHelpFormatter | Appends (default: X) to every argument's help text. |
metavar e %(default)s
Controle o que aparece nas mensagens 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)")Isso produz --top N no texto de uso em vez de --top TOP, e o texto de ajuda mostra o valor padrão real.
argparse vs sys.argv vs click vs typer
Python tem várias abordagens para parsing de argumentos de linha de comando. Veja como elas se comparam:
| Feature | sys.argv | argparse | click | typer |
|---|---|---|---|---|
| Part of stdlib | Yes | Yes | No (pip install) | No (pip install) |
| Type conversion | Manual | Built-in | Built-in | Automatic (type hints) |
| Help generation | None | Automatic | Automatic | Automatic |
| Subcommands | Manual | add_subparsers() | @group.command() | app.command() |
| Validation | Manual | choices, custom type | click.Choice, callbacks | Validators |
| Boilerplate | High | Medium | Low | Very low |
| API style | Imperative | Imperative | Decorators | Decorators + type hints |
| Tab completion | None | None | Via plugin | Built-in |
| Prompting / colors | Manual | None | Built-in | Built-in |
| Testing | Manual | parse_args([...]) | CliRunner | CliRunner |
| Best for | Throwaway scripts | Stdlib-only projects | Complex CLIs | Modern Python 3.7+ |
Aqui está a mesma ferramenta implementada com cada abordagem:
# --- 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)Quando usar cada um:
- sys.argv -- Apenas para scripts únicos em que você pega um único valor e não se importa com validação.
- argparse -- Quando você precisa de uma CLI de verdade, mas não pode adicionar dependências externas. Esta é a escolha padrão certa para a maioria dos projetos.
- click -- Quando você está construindo uma CLI complexa com grupos de comandos aninhados, prompts interativos, saída colorida e sistemas de plugins.
- typer -- Quando você quer o mínimo de boilerplate e usa Python 3.7+. Ele infere argumentos a partir de type hints e se baseia em click.
Projeto real: CLI processador de dados CSV
Aqui está uma ferramenta CLI completa, com estilo de produção, que lê arquivos CSV, filtra dados, calcula agregações e gera resultados em múltiplos formatos. Isso mostra como os padrões do argparse se combinam em um projeto 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 ferramenta combina subcomandos, tratamento de tipos personalizados, múltiplos formatos de saída, grupos de argumentos e tratamento adequado de erros. Cada subcomando tem seu próprio conjunto focado de argumentos. Os usuários obtêm --help completo para cada subcomando.
Visualizando os resultados: Depois de filtrar ou agregar dados CSV com uma ferramenta CLI como esta, você geralmente quer explorar a saída visualmente. O PyGWalker (opens in a new tab) transforma qualquer DataFrame do pandas em uma interface de visualização interativa, estilo Tableau -- diretamente em um notebook Jupyter ou script. Envie a saída da sua CLI para um DataFrame e use o PyGWalker para criar gráficos sem escrever código de plotagem:
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 desenvolvimento interativo de ferramentas CLI como esta, o RunCell (opens in a new tab) permite construir e testar ferramentas CLI dentro do Jupyter com assistência de IA. Você pode prototipar a lógica de parsing de argumentos em células do notebook, testar diferentes combinações de argumentos e depois exportar para um script independente.
Erros comuns e correções
1. error: unrecognized arguments
Isso acontece quando você passa um argumento que não foi definido:
$ python tool.py --output results.csv --compress
error: unrecognized arguments: --compressCorreção: Adicione o argumento ao seu parser, ou use parse_known_args() se você intencionalmente quiser ignorar argumentos desconhecidos:
args, unknown = parser.parse_known_args()
# args contains recognized arguments
# unknown is a list of unrecognized strings2. error: argument --count: invalid int value
$ python tool.py --count three
error: argument --count: invalid int value: 'three'Correção: Isso é o argparse funcionando corretamente. O usuário passou uma string não numérica para um argumento com type=int. Suas funções de tipo personalizadas devem lançar ArgumentTypeError com uma mensagem clara.
3. Nomes de atributos com hífens
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 underscores4. Subcomando não dispara nenhuma ação
# 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)Sem required=True, executar o script sem subcomando não faz nada silenciosamente. No Python 3.7+, sempre defina required=True para subparsers.
5. Mensagens de ajuda duplicadas por propagação
Se um parser pai e um subparser definirem o mesmo argumento, os usuários ficam confusos:
# 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() sendo executado na importação
# 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 arquivos cedo demais
# 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")Padrões avançados
Parent parsers para argumentos compartilhados
Quando vários subcomandos compartilham o mesmo conjunto de argumentos, use parent parsers para evitar repetição:
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 padrão de variáveis de ambiente
Busque valores padrão em variáveis de ambiente para flexibilidade em deploy:
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)")Testando scripts argparse
Você pode integrar testes de argparse em suas suítes de unittest ou pytest. Teste suas ferramentas CLI passando listas de argumentos para 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 == 5Separar create_parser() de main() torna suas definições de argumentos testáveis sem executar o script inteiro.
FAQ
Para que serve o Python argparse?
Python argparse é o módulo da biblioteca padrão para analisar argumentos de linha de comando. Ele lê strings de sys.argv, converte-as em objetos Python tipados, valida entradas, gera saída de --help e relata erros quando os usuários fornecem argumentos incorretos. Ele é usado para transformar scripts em ferramentas CLI adequadas que aceitam entradas configuráveis sem editar o código-fonte.
Qual é a diferença entre argumentos posicionais e opcionais no argparse?
Argumentos posicionais não têm hífens no nome (por exemplo, parser.add_argument("filename")). Eles são obrigatórios por padrão e correspondem à posição. Argumentos opcionais começam com hífens (por exemplo, parser.add_argument("--verbose")). Eles são opcionais por padrão e podem aparecer em qualquer ordem. Você pode tornar argumentos opcionais obrigatórios com required=True.
Como crio subcomandos como git commit ou docker build?
Use parser.add_subparsers() para criar um grupo de subcomandos. Chame subparsers.add_parser("name") para cada subcomando. Cada subparser recebe seu próprio conjunto de argumentos. Use set_defaults(func=handler_function) para associar cada subcomando a um handler e, em seguida, chame args.func(args) para fazer o despacho.
Devo usar argparse ou click para minha CLI em Python?
Use argparse quando você precisa de zero dependências externas e sua CLI é direta. Use click quando precisar de recursos avançados como prompts interativos, saída colorida no terminal, barras de progresso ou grupos de comandos profundamente aninhados. Typer é outra opção que usa type hints do Python e exige ainda menos boilerplate que click.
Como valido tipos de entrada personalizados com argparse?
Escreva uma função que receba uma string e retorne o valor convertido. Se a entrada for inválida, lance argparse.ArgumentTypeError com uma mensagem descritiva. Passe essa função como parâmetro type: parser.add_argument("--date", type=my_date_parser). O argparse chama sua função automaticamente e exibe a mensagem de erro se a validação falhar.
Posso usar argparse com variáveis de ambiente?
Sim. Defina o parâmetro default para ler de os.environ: parser.add_argument("--api-key", default=os.environ.get("API_KEY")). Isso permite que os usuários configurem valores por meio de variáveis de ambiente, enquanto as flags de linha de comando ainda podem sobrescrevê-los.
Conclusão
O Python argparse transforma scripts de ferramentas frágeis, que exigem edição do código-fonte, em programas de linha de comando adequados. O módulo cuida da conversão de tipos, validação, geração de ajuda e mensagens de erro, para que você foque no que o script faz, e não em como ele recebe entrada.
Os padrões para lembrar:
- Use argumentos posicionais para entradas obrigatórias e argumentos com hífens para os opcionais.
- Defina
typepara conversão automática,choicespara valores restritos enargspara múltiplos valores. - Use
action="store_true"para flags booleanas eaction="count"para níveis de verbosidade. - Construa ferramentas complexas com
add_subparsers()e o padrão de despachoset_defaults(func=handler). - Use grupos mutuamente exclusivos quando os argumentos entrarem em conflito.
- Envolva o parsing em
main()e proteja-o comif __name__ == "__main__". - Escreva funções de tipo personalizadas para validação específica do domínio.
argparse não é a biblioteca de parsing de argumentos mais nova do Python. Mas ele vem com toda instalação do Python, cobre a grande maioria das necessidades de CLI e produz ferramentas que se comportam como os usuários esperam. Para a maioria dos desenvolvedores Python, essa é a escolha certa. Quando sua ferramenta CLI precisar invocar comandos externos, combine argparse com o módulo subprocess para uma solução completa de automação em linha de comando.
Guias relacionados
- Python subprocess -- Execute comandos externos a partir das suas ferramentas CLI
- Python pathlib -- Manipulação moderna de caminhos de arquivo para argumentos de CLI
- Python type hints -- Adicione anotações de tipo às suas funções CLI
- Python unittest -- Escreva testes para suas ferramentas baseadas em argparse