Typer transforma funções anotadas em comandos de terminal. Parâmetros obrigatórios viram argumentos, valores com padrão viram opções e os type hints orientam conversão, validação e documentação. Ele é especialmente útil para ferramentas internas e automações que já dependem de pacotes externos.
Se o projeto precisa apenas de um comando pequeno e deve usar somente a biblioteca padrão, o guia de argparse continua sendo a escolha mais simples.
Criar o primeiro comando
Instale em um ambiente virtual:
python -m pip install typer
Crie app.py:
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer(no_args_is_help=True)
@app.command()
def contar(
arquivo: Path,
ignorar_vazias: Annotated[
bool, typer.Option("--ignorar-vazias/--manter-vazias")
] = True,
) -> None:
"""Conta linhas de um arquivo de texto."""
if not arquivo.is_file():
raise typer.BadParameter("o arquivo não existe")
linhas = arquivo.read_text(encoding="utf-8").splitlines()
total = sum(bool(linha.strip()) for linha in linhas) if ignorar_vazias else len(linhas)
typer.echo(total)
if __name__ == "__main__":
app()
Execute python app.py --help antes de testar o comando. A ajuda é parte da interface pública: escreva docstrings objetivas e escolha nomes que façam sentido no terminal.
Organizar subcomandos
Para uma ferramenta maior, agrupe ações como usuarios criar e usuarios listar em objetos Typer separados. As funções de comando devem converter entrada e apresentar saída. Regras de negócio, acesso a arquivos e chamadas HTTP pertencem a módulos testáveis sem a CLI.
Não coloque chaves de API em opções visíveis no histórico do shell. Prefira variável de ambiente ou entrada oculta com prompt=True e hide_input=True, conforme o risco.
Testar sem subprocesso
from typer.testing import CliRunner
from app import app
runner = CliRunner()
def test_ajuda() -> None:
resultado = runner.invoke(app, ["--help"])
assert resultado.exit_code == 0
assert "Conta linhas" in resultado.stdout
Teste também arquivo inexistente, encoding inválido e combinações de opções. O artigo sobre pytest em Python ajuda a estruturar esses cenários.
A documentação oficial do Typer, consultada em 22 de julho de 2026, detalha argumentos, opções, subcomandos e testes. Adote Typer pelo ganho de ergonomia, não apenas pela ajuda colorida: uma boa CLI também precisa de códigos de saída previsíveis, mensagens acionáveis e comportamento seguro.
Argumentos, opções e metadados
Argumentos identificam o recurso principal; opções ajustam o comportamento. Evite muitos posicionais, pois o usuário precisa decorar a ordem. Annotated mantém tipo e metadados juntos:
def exportar(
origem: Annotated[Path, typer.Argument(help="Arquivo JSON de entrada")],
saida: Annotated[
Path, typer.Option("--saida", "-o", help="Arquivo de destino")
] = Path("relatorio.csv"),
limite: Annotated[
int, typer.Option(min=1, max=10_000, help="Máximo de linhas")
] = 100,
) -> None:
...
Typer converte o valor antes de chamar a função e rejeita um inteiro inválido. Validação de domínio continua na aplicação: um caminho válido pode conter dados no formato errado.
Pares como --cor/--sem-cor tornam os estados claros. Se houver três modos, prefira um enum. Preserve nomes de opções, pois scripts dependem deles.
Erros úteis e códigos de saída
Use typer.BadParameter para entrada inválida. Em falha operacional, escreva diagnóstico em stderr e encerre com código diferente de zero:
try:
linhas = carregar(arquivo)
except PermissionError:
typer.echo(f"Não foi possível ler {arquivo}", err=True)
raise typer.Exit(code=2)
Não mostre traceback para um erro comum do usuário, mas também não capture todo Exception e apague o contexto. Defeitos inesperados precisam continuar observáveis. Defina um contrato de códigos quando outro programa consome a CLI: zero para sucesso e códigos documentados somente quando o chamador pode reagir.
Saída também é interface. Envie dados para stdout e diagnósticos para stderr. Para automação, ofereça --json estável em vez de obrigar scripts a interpretar tabelas coloridas.
Organizar subcomandos
Crie um objeto por grupo e registre com add_typer:
app = typer.Typer(no_args_is_help=True)
usuarios = typer.Typer(no_args_is_help=True)
@usuarios.command("listar")
def listar_usuarios(ativos: bool = True) -> None:
for usuario in buscar_usuarios(ativos=ativos):
typer.echo(usuario.nome)
app.add_typer(usuarios, name="usuarios")
Separe grupos em módulos quando o arquivo crescer. Evite importações circulares criando a aplicação em um módulo pequeno. Funções de comando traduzem valores da CLI para serviços Python; os serviços não devem importar Typer.
Callbacks servem para opções globais, como --verbose e caminho de configuração. Um typer.Context pode carregar estado compartilhado, mas não transforme context.obj em dependência invisível da regra de negócio.
Configuração, prompts e segredos
Documente a precedência entre opção, variável de ambiente, arquivo e padrão. Uma ordem previsível é opção explícita, ambiente, arquivo e valor padrão.
Prompt oculto evita gravar senha no histórico, mas impede execução sem interação. Em CI, use um gerenciador de segredos ou variável fornecida pela plataforma. Nunca mostre credenciais em logs ou exceções.
Confirmações são úteis para ações destrutivas. Mantenha a resposta segura como padrão e ofereça uma opção explícita, como --sim, para automação controlada.
Testar além do texto de ajuda
def test_arquivo_ausente(tmp_path: Path) -> None:
ausente = tmp_path / "ausente.txt"
result = runner.invoke(app, ["count", str(ausente)])
assert result.exit_code != 0
assert "não existe" in result.output
def test_contagem(tmp_path: Path) -> None:
origem = tmp_path / "linhas.txt"
origem.write_text("uma\n\ntrês\n", encoding="utf-8")
result = runner.invoke(app, ["count", str(origem)])
assert result.exit_code == 0
assert result.stdout.strip() == "2"
Teste a invocação pública, incluindo nomes de comandos, e teste os serviços extraídos diretamente. Isso cobre regressões da interface sem fazer toda regra de negócio passar pela emulação do terminal. O tutorial de pytest detalha fixtures e organização.
Instalar como comando
Declare o entry point no pyproject.toml:
[project.scripts]
ferramentas-texto = "ferramentas_texto.cli:app"
Depois da instalação, o usuário executa ferramentas-texto, sem depender de python app.py. Mantenha o módulo de entrada leve para que --help abra rápido. Declare versões suportadas de Python e Typer e teste o comando instalado na CI.
Typer reduz código de parsing, mas não substitui design de interface. Ajuda clara, nomes estáveis, saída previsível, segredos protegidos e testes tornam o comando confiável.
Checklist para publicar a CLI
Execute cada comando com --help e confira nomes, padrões, exemplos e termos. Teste tipos inválidos, arquivos ausentes, falta de permissão, entrada vazia e opções conflitantes. Diagnósticos devem ir para stderr; dados consumidos por máquina devem permanecer limpos em stdout.
Instale o pacote em ambiente virtual novo, em vez de testar apenas o checkout. Rode o entry point a partir de outra pasta para encontrar dependências acidentais do diretório atual. Teste caminhos com espaços e caracteres não ASCII nos sistemas suportados.
Trate comandos, opções, códigos de saída e campos JSON como API pública. Quando scripts puderem depender de um nome, deprecie antes de remover e registre a mudança. Preserve um teste de ponta a ponta do fluxo de automação mais importante.
Confirme ainda que Ctrl+C interrompe rapidamente, arquivos temporários são removidos, escritas importantes são atômicas e a repetição de uma operação segura tem resultado previsível. Esses detalhes valem mais que decoração elaborada no terminal.
Evoluir sem quebrar automações
Antes de renomear uma opção, procure usos em documentação, CI e scripts. Preserve temporariamente o alias antigo, indique a forma preferida na ajuda e remova somente em versão comunicada. Alterar descrição é simples; mudar o significado de uma opção existente quebra expectativas.
Ao adicionar um novo padrão, teste invocações antigas. Uma mudança aparentemente pequena pode selecionar mais registros ou sobrescrever arquivos. Prefira padrões conservadores e exija confirmação para efeitos destrutivos.
Se houver saída JSON consumida externamente, trate o esquema como contrato. Mensagens humanas podem melhorar, mas nomes e tipos de campos precisam de compatibilidade. Mantenha exemplos pequenos e testes de contrato.
Para arquivos grandes, processe em streaming e mostre progresso apenas em terminal interativo. Saída redirecionada não deve receber animações. Ofereça uma opção explícita para desativar progresso.
Documente como caminhos relativos e arquivos de configuração são resolvidos. Normalmente o diretório atual é o ponto mais previsível, mas o comportamento precisa de teste a partir de outra pasta.