argparse é o módulo da biblioteca padrão para transformar argumentos do terminal em dados Python, gerar ajuda e informar erros de uso. A resposta direta é: crie um ArgumentParser, declare argumentos e opções, chame parse_args() e encaminhe o resultado para funções independentes. Para aplicações com várias ações, use subcommands; para executar a ferramenta pelo nome, publique um entry point no pyproject.toml.
Neste tutorial construiremos tarefas, uma CLI que adiciona e lista itens em um arquivo JSON. Ela aceitará argumentos posicionais, opções curtas e longas, subcommands, valores restritos e um modo verboso. Como argparse já acompanha Python, não há dependência externa. Trabalhe em um ambiente virtual com venv para testar a instalação sem afetar outras ferramentas.
Argumentos posicionais e opções
Um argumento posicional identifica um valor pela posição. Uma opção começa com - ou --, costuma ser facultativa e pode ter valor padrão. Este exemplo mínimo recebe um arquivo e permite escolher a saída:
import argparse
parser = argparse.ArgumentParser(description="Resume um arquivo de tarefas")
parser.add_argument("arquivo", help="caminho do arquivo JSON")
parser.add_argument("-f", "--formato", choices=["texto", "json"], default="texto")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
print(args.arquivo, args.formato, args.verbose)
choices rejeita valores desconhecidos e os exibe na ajuda. store_true começa como False e vira True quando a flag aparece. Use type=int ou uma função para converter entradas, mas lembre que tudo chega inicialmente como texto. A referência oficial de argparse documenta cada ação e parâmetro.
Execute o programa com python app.py dados.json --formato json -v. Rode também python app.py --help: ajuda útil faz parte da interface, não é detalhe. Nomes claros, descrições objetivas e defaults visíveis evitam consultas ao código-fonte.
Projeto prático e estrutura
Crie esta estrutura:
gerenciador-tarefas/
├── pyproject.toml
├── src/
│ └── tarefas_cli/
│ ├── __init__.py
│ └── cli.py
└── tests/
└── test_cli.py
O código ficará em src/tarefas_cli/cli.py. A função build_parser monta a interface; main interpreta a entrada; funções de comando executam a regra. Essa separação torna os testes simples e mantém o parser fora do domínio. Para revisar organização e imports, consulte módulos e pacotes em Python.
import argparse
import json
from pathlib import Path
from typing import Sequence
PRIORIDADES = ("baixa", "media", "alta")
def carregar(caminho: Path) -> list[dict]:
if not caminho.exists():
return []
return json.loads(caminho.read_text(encoding="utf-8"))
def salvar(caminho: Path, tarefas: list[dict]) -> None:
caminho.parent.mkdir(parents=True, exist_ok=True)
caminho.write_text(
json.dumps(tarefas, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def adicionar(args: argparse.Namespace) -> int:
tarefas = carregar(args.arquivo)
tarefas.append({"titulo": args.titulo, "prioridade": args.prioridade})
salvar(args.arquivo, tarefas)
print(f"Tarefa adicionada: {args.titulo}")
return 0
def listar(args: argparse.Namespace) -> int:
tarefas = carregar(args.arquivo)
filtradas = [
item for item in tarefas
if args.prioridade is None or item["prioridade"] == args.prioridade
]
if args.como_json:
print(json.dumps(filtradas, ensure_ascii=False, indent=2))
else:
for indice, item in enumerate(filtradas, start=1):
print(f"{indice}. [{item['prioridade']}] {item['titulo']}")
return 0
Path evita concatenação manual de caminhos; há mais exemplos no guia de pathlib em Python. Retornar um inteiro de cada comando estabelece o código de saída: zero significa sucesso, enquanto valores diferentes de zero indicam falha para shells e automações.
Subcommands com parsers específicos
add_subparsers() cria comandos filhos. Cada um recebe somente os argumentos relevantes e registra sua função com set_defaults(func=...):
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="tarefas",
description="Gerencia tarefas em um arquivo JSON.",
)
parser.add_argument(
"--arquivo",
type=Path,
default=Path("tarefas.json"),
help="arquivo de dados (padrão: tarefas.json)",
)
subparsers = parser.add_subparsers(dest="comando", required=True)
parser_adicionar = subparsers.add_parser("adicionar", help="cria uma tarefa")
parser_adicionar.add_argument("titulo", help="texto da tarefa")
parser_adicionar.add_argument(
"-p", "--prioridade", choices=PRIORIDADES, default="media"
)
parser_adicionar.set_defaults(func=adicionar)
parser_listar = subparsers.add_parser("listar", help="mostra tarefas")
parser_listar.add_argument("-p", "--prioridade", choices=PRIORIDADES)
parser_listar.add_argument("--json", dest="como_json", action="store_true")
parser_listar.set_defaults(func=listar)
return parser
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
O parâmetro opcional argv é importante: em produção, None faz argparse ler sys.argv; no teste, uma lista controla a chamada. O bloco final permite python -m tarefas_cli.cli sem executar código durante o import. Entenda melhor esse padrão no artigo sobre __name__ e __main__.
Experimente:
python -m tarefas_cli.cli adicionar "Revisar relatório" --prioridade alta
python -m tarefas_cli.cli listar
python -m tarefas_cli.cli --arquivo equipe.json listar --json
Opções do parser principal normalmente precisam aparecer antes do subcommand. Se --arquivo também precisar funcionar depois dele, declare-a nos parsers filhos ou use um parser pai compartilhado. Evite aceitar duas ordens sem necessidade, pois interfaces previsíveis são mais fáceis de documentar.
Validação e mensagens de erro
Use choices para conjuntos fechados e type para conversão. Uma função de tipo pode validar condições específicas e levantar ArgumentTypeError:
def inteiro_positivo(valor: str) -> int:
numero = int(valor)
if numero < 1:
raise argparse.ArgumentTypeError("deve ser maior que zero")
return numero
Não capture o SystemExit produzido por erro de sintaxe na aplicação normal. Ele fornece código 2 e uma mensagem consistente. Já erros operacionais, como JSON corrompido ou permissão negada, devem ser tratados perto de main, enviados para stderr e convertidos em código de saída adequado. O guia de tratamento de erros em Python ajuda a não esconder exceções inesperadas.
import sys
def main(argv: Sequence[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
return args.func(args)
except (OSError, json.JSONDecodeError) as exc:
print(f"erro: {exc}", file=sys.stderr)
return 1
Não use except Exception apenas para imprimir uma mensagem genérica: isso esconde defeitos de programação e dificulta diagnóstico.
Entry point instalável
Um entry point associa o comando tarefas a uma função Python. O pyproject.toml mínimo é:
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[project]
name = "tarefas-cli"
version = "0.1.0"
requires-python = ">=3.10"
[project.scripts]
tarefas = "tarefas_cli.cli:main"
Instale em modo editável com python -m pip install -e . e execute tarefas --help. Não coloque parênteses em :main; o instalador cria um wrapper que chamará a função. A especificação de entry points da PyPA explica o formato, e o tutorial de HOWTO oficial de argparse reforça a evolução de CLIs simples. Para distribuição pública, veja como publicar um pacote no PyPI.
Testes da CLI
Teste main sem subprocesso para obter velocidade e mensagens com capsys:
from tarefas_cli.cli import main
def test_adicionar_e_listar(tmp_path, capsys):
arquivo = tmp_path / "tarefas.json"
assert main(["--arquivo", str(arquivo), "adicionar", "Estudar"]) == 0
capsys.readouterr()
assert main(["--arquivo", str(arquivo), "listar"]) == 0
saida = capsys.readouterr().out
assert "[media] Estudar" in saida
def test_prioridade_invalida_encerra():
import pytest
with pytest.raises(SystemExit) as erro:
main(["adicionar", "Estudar", "--prioridade", "urgente"])
assert erro.value.code == 2
Também vale um teste de integração que execute o comando instalado, mas a maior parte das regras deve permanecer em funções comuns. O tutorial de pytest em Python mostra fixtures e parametrização para ampliar esses cenários.
Compatibilidade, saída e automação
Uma CLI passa a ser uma interface pública assim que outro script começa a chamá-la. Por isso, alterar o nome de uma opção, trocar o formato da saída ou reutilizar um código de saída pode quebrar automações mesmo que a regra de negócio continue correta. Prefira acrescentar opções compatíveis e documentar uma descontinuação antes de remover comportamento existente. Se o comando tiver consumidores automatizados, ofereça uma saída estável, como --format json, separada do texto amigável exibido para pessoas.
Adote uma convenção simples para os fluxos: dados normais vão para stdout, mensagens de diagnóstico vão para stderr e o retorno zero indica sucesso. Um erro de uso já é tratado pelo argparse com código 2; falhas operacionais podem retornar 1. Essa separação permite redirecionar o resultado para um arquivo sem misturá-lo com avisos:
tarefas listar --format json > tarefas.json
Ao evoluir o parser, teste também --help. Verifique se cada subcommand explica o objetivo, se opções booleanas deixam claro o padrão e se exemplos usam exatamente a sintaxe aceita. Não exponha segredos como argumentos de linha de comando, pois eles podem aparecer no histórico do shell ou na lista de processos. Tokens e senhas devem vir de uma fonte apropriada, como variável de ambiente ou entrada segura. Esse cuidado transforma um script conveniente em uma ferramenta previsível para terminal, CI e tarefas agendadas.
Perguntas frequentes
argparse precisa ser instalado?
Não. Ele integra a biblioteca padrão do Python. Dependências só serão necessárias para outras funcionalidades da aplicação.
Quando usar argumento posicional ou opção?
Use posicionais para entradas essenciais e poucas. Use opções para configurações facultativas, flags e valores com defaults.
Subcommand é melhor que várias flags?
Quando ações têm argumentos diferentes, sim. adicionar e listar ficam mais claros que combinações como --add --list.
Entry point e bloco __main__ são a mesma coisa?
Não. O entry point cria um comando após a instalação; o bloco permite executar o módulo diretamente. Ambos podem chamar a mesma main.
Conclusão
Uma CLI sólida separa parsing de regra de negócio, oferece ajuda legível, valida a entrada e retorna códigos úteis. Com subcommands, main(argv), testes e [project.scripts], o pequeno gerenciador já funciona tanto para pessoas quanto para automações. Comece com a interface mínima e trate nomes, mensagens e compatibilidade como parte da API pública.