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.
- Atualize código, documentação, changelog e versão.
- Execute testes e type checker, se adotado.
- Construa do zero e rode
twine check. - Instale a wheel em ambiente limpo e faça um smoke test.
- Crie a tag e publique um GitHub Release; o workflow envia os arquivos.
Erros comuns ao publicar
- Nome já ocupado: o
project.nameprecisa 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__.pye a configuração[tool.setuptools.packages.find]. - README quebrado: execute
twine checke evite recursos locais ou HTML não aceito. - Arquivos indesejados: inspecione a sdist e a wheel; não presuma que
.gitignorecontrola 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
srce 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.