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.