Para criar e publicar um pacote Python moderno no PyPI, coloque o código em um layout src, descreva projeto e build no pyproject.toml, gere uma wheel e uma sdist com python -m build, valide com twine check e teste primeiro no TestPyPI. Para releases reais, a opção mais segura é conectar o PyPI ao GitHub Actions com Trusted Publishing, sem guardar tokens de longa duração.

Este guia percorre o processo completo com um pequeno pacote chamado saudacao-clara. Antes de começar, vale revisar a diferença entre módulos e pacotes Python, criar um ambiente virtual isolado e entender como o pip gerencia dependências.

Pacote importável não é distribuição

Os termos são próximos, mas representam camadas diferentes. Um pacote de importação é o diretório que o Python encontra em um comando como import saudacao_clara. Uma distribuição é o artefato instalável publicado em um índice: por exemplo, saudacao_clara-0.1.0-py3-none-any.whl ou o arquivo fonte .tar.gz.

O nome da distribuição no PyPI pode conter hífen, enquanto o pacote importado usa um identificador Python com sublinhado. Assim, o usuário executa pip install saudacao-clara e escreve from saudacao_clara import saudar. Não confunda também PyPI com pip: o primeiro hospeda distribuições; o segundo resolve e instala projetos.

Estrutura recomendada com layout src

Crie uma pasta vazia e adote esta árvore:

saudacao-clara/
├── .github/
│   └── workflows/
│       └── publish.yml
├── src/
│   └── saudacao_clara/
│       ├── __init__.py
│       └── core.py
├── tests/
│   └── test_core.py
├── LICENSE
├── README.md
└── pyproject.toml

O layout src impede que testes importem acidentalmente o código direto da raiz do repositório. Para testar, você instala o projeto, exercitando uma estrutura mais próxima daquela recebida pelo usuário. Ele também separa com clareza código importável de documentação, automação e configuração.

O arquivo __init__.py torna explícito o pacote regular e define sua API pública. Não coloque testes, README ou arquivos de workflow dentro de src. Se o projeto crescer, subpacotes ficam abaixo de src/saudacao_clara/.

Um pyproject.toml atual com setuptools

O pyproject.toml centraliza duas informações antes espalhadas por vários arquivos: como construir o projeto e quais metadados serão publicados. A especificação oficial do pyproject.toml distingue as tabelas padronizadas das configurações específicas de ferramentas.

[build-system]
requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"

[project]
name = "saudacao-clara"
version = "0.1.0"
description = "Saudações pequenas e previsíveis para exemplos Python"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
authors = [
  { name = "Seu Nome", email = "[email protected]" },
]
keywords = ["saudacao", "exemplo", "tutorial"]
classifiers = [
  "Programming Language :: Python :: 3",
  "Operating System :: OS Independent",
]
dependencies = []

[project.optional-dependencies]
test = ["pytest>=8"]

[project.urls]
Homepage = "https://github.com/seu-usuario/saudacao-clara"
Issues = "https://github.com/seu-usuario/saudacao-clara/issues"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]

O bloco build-system diz a um frontend de build que use setuptools em um ambiente isolado. Já [project] segue um padrão compartilhado por backends. Troque nome, descrição, autor e URLs; principalmente, pesquise no PyPI um nome de distribuição disponível. O campo requires-python deve refletir as versões que você realmente testa, não apenas a mais recente instalada na sua máquina.

Dependências necessárias em tempo de execução entram em dependencies. Ferramentas de teste não devem ser impostas a todo usuário; por isso ficam no extra test. Para aprofundar contratos de API, consulte também nosso guia de type hints em Python.

README e licença fazem parte do produto

O README.md aparece na página do projeto e precisa responder rapidamente o que o pacote faz, como instalar e qual é o exemplo mínimo. Use links absolutos para imagens, pois caminhos relativos podem funcionar no GitHub e quebrar no PyPI. Um começo suficiente seria:

## saudacao-clara

Uma biblioteca pequena para produzir saudações consistentes.

## Instalação

`python -m pip install saudacao-clara`

## Uso

```python
from saudacao_clara import saudar

print(saudar("Ada"))
```

Escolha deliberadamente uma licença e inclua o texto completo em LICENSE. O exemplo usa a expressão SPDX MIT; não a mantenha se essa não for sua decisão. Sem uma licença, ter o código visível não concede automaticamente permissão ampla para reutilizá-lo.

Código mínimo e teste pela API pública

Em src/saudacao_clara/core.py:

def saudar(nome: str) -> str:
    """Retorna uma saudação para um nome não vazio."""
    nome_limpo = nome.strip()
    if not nome_limpo:
        raise ValueError("nome não pode ser vazio")
    return f"Olá, {nome_limpo}!"

Em src/saudacao_clara/__init__.py, exponha só o contrato que o consumidor deve usar:

from .core import saudar

__all__ = ["saudar"]

E em tests/test_core.py:

import pytest

from saudacao_clara import saudar


def test_saudar_remove_espacos() -> None:
    assert saudar("  Ada  ") == "Olá, Ada!"


def test_saudar_rejeita_nome_vazio() -> None:
    with pytest.raises(ValueError, match="não pode ser vazio"):
        saudar("  ")

Crie e ative seu ambiente virtual, então instale o projeto em modo editável com o extra de testes:

python -m pip install --upgrade pip
python -m pip install -e ".[test]"
python -m pytest

O editável reflete mudanças no código sem reinstalação manual. Para uma estratégia além deste exemplo, veja o guia de testes automatizados com pytest.

Gerar wheel e sdist e validar

Instale as ferramentas de empacotamento, apague artefatos antigos antes de um novo release e construa:

python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*

O build isolado cria dist/saudacao_clara-0.1.0-py3-none-any.whl e uma sdist .tar.gz. A wheel é pronta para instalação e, neste projeto Python puro, funciona em qualquer plataforma compatível. A sdist contém fontes e será construída pelo instalador. Publique ambas: wheel oferece instalação eficiente e sdist preserva uma alternativa de fonte.

twine check valida metadados e renderização do README, mas não prova que o pacote importa. Antes de publicar, crie outro ambiente limpo, instale a wheel pelo caminho e execute um pequeno import. A sequência completa está alinhada ao tutorial oficial de empacotamento.

Ensaiar no TestPyPI

TestPyPI é uma instância separada: exige cadastro próprio e não compartilha projetos nem credenciais com o PyPI real. Para um ensaio manual, crie um token no TestPyPI e envie os artefatos:

python -m twine upload --repository testpypi dist/*

Quando solicitado, use __token__ como usuário e o token completo como senha. Não grave a credencial no repositório ou no histórico do shell. Depois, teste em um ambiente novo:

python -m pip install --index-url https://test.pypi.org/simple/ --no-deps saudacao-clara
python -c "from saudacao_clara import saudar; print(saudar('Ada'))"

--no-deps é adequado aqui porque o exemplo não possui dependências. Em um pacote que possui, o índice de teste pode não conter todas elas; prefira instalar dependências separadamente em vez de misturar índices sem avaliar o risco de dependency confusion.

Publicar no PyPI pelo GitHub Actions sem token

Trusted Publishing usa OIDC para entregar uma credencial curta apenas ao workflow configurado. No PyPI, crie o projeto antecipadamente ou use um publisher pendente; informe proprietário, repositório, nome exato do workflow publish.yml e environment pypi. A documentação de Trusted Publishers do PyPI contém as telas e restrições atuais.

Adicione .github/workflows/publish.yml:

name: Publish package

on:
  release:
    types: [published]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: python -m pip install --upgrade build
      - run: python -m build
      - uses: actions/upload-artifact@v4
        with:
          name: python-package-distributions
          path: dist/

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/saudacao-clara
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1

A permissão id-token: write permite solicitar a identidade OIDC; ela não dá escrita genérica no repositório. Proteja o environment pypi com aprovação se o processo exigir revisão. Idealmente, a integração contínua testa commits e pull requests, enquanto este workflow só publica um GitHub Release. Nosso guia de CI/CD Python com GitHub Actions mostra como separar essas responsabilidades.

Versionamento e rotina de release

Uma versão publicada não pode ser sobrescrita. Adote versões compreensíveis, como MAJOR.MINOR.PATCH: aumente PATCH para correções compatíveis, MINOR para recursos compatíveis e MAJOR para mudanças incompatíveis. Pré-releases podem usar 0.2.0rc1. O importante é que a versão em pyproject.toml, a tag e as notas do release contem a mesma história.

  1. Atualize código, documentação, changelog e versão.
  2. Execute testes e type checker, se adotado.
  3. Construa do zero e rode twine check.
  4. Instale a wheel em ambiente limpo e faça um smoke test.
  5. Crie a tag e publique um GitHub Release; o workflow envia os arquivos.

Erros comuns ao publicar

  • Nome já ocupado: o project.name precisa ser único no índice, mesmo que o nome de importação seja diferente.
  • Package não encontrado: confirme o diretório sob src, o __init__.py e a configuração [tool.setuptools.packages.find].
  • README quebrado: execute twine check e evite recursos locais ou HTML não aceito.
  • Arquivos indesejados: inspecione a sdist e a wheel; não presuma que .gitignore controla o conteúdo distribuído.
  • Upload recusado: não tente reenviar a mesma versão. Incremente-a e reconstrua sem resíduos em dist.
  • Workflow não confiável: proprietário, repositório, arquivo e environment devem coincidir exatamente com o publisher cadastrado.

Checklist antes do primeiro release

  • Nome no PyPI verificado e metadados revisados.
  • Layout src e API pública importável.
  • README renderizável, licença correta e URLs válidas.
  • Versões de Python declaradas realmente cobertas pelos testes.
  • Testes verdes em ambiente limpo.
  • Wheel e sdist novas, inspecionadas e aprovadas pelo twine check.
  • Instalação e import testados via TestPyPI.
  • Trusted Publisher e environment do GitHub configurados sem token persistente.
  • Versão, tag e notas de release coerentes.

Empacotar bem não é apenas fazer o upload funcionar. É oferecer uma instalação previsível, metadados úteis, licença clara e um processo que possa ser repetido sem improviso no próximo release. Com essa base, o mesmo fluxo atende tanto uma biblioteca pequena quanto um projeto que ganhará múltiplos módulos e colaboradores.