_SH Log's
Back to Root
EST: 4 min read

How I Shipped a Python Package to PyPI in 2026

A real walkthrough of publishing a Python package to PyPI in 2026: pyproject with hatchling, src layout, tests and trusted publishing — using freelm.

#python#pypi#open-source#packaging#build-in-public

To ship a Python package to PyPI in 2026, use a pyproject.toml with a modern build backend (hatchling), an src/ layout, tests, and a GitHub Actions release that publishes on a tagged version. I just did this for freelm, my free LLM gateway, so here's the exact path that worked.

1. Project layout: use src/

An src/ layout puts your package one directory down, which forces tests to run against the installed package rather than the working directory — catching packaging bugs before your users do.

freelm/
  pyproject.toml
  src/freelm/__init__.py
  tests/

2. pyproject.toml with hatchling

One file declares everything: metadata, dependencies, and the build backend. Keep runtime dependencies minimal — freelm depends only on httpx.

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "freelm"
version = "0.2.2"
requires-python = ">=3.9"
dependencies = ["httpx>=0.24"]

[tool.hatch.build.targets.wheel]
packages = ["src/freelm"]

The name must be unique on PyPI — check availability before you commit to it.

3. Make it discoverable

PyPI metadata is SEO. A keyword-rich description, a real keywords list, and accurate classifiers (including Typing :: Typed if you ship a py.typed marker) decide whether anyone finds the package. Spend time on the README too — it becomes the PyPI long description.

4. Build and check before uploading

Build both a wheel and an sdist, then validate the metadata with twine check so you don't publish a broken description:

python -m build
python -m twine check dist/*

5. Publish with Trusted Publishing

In 2026 the clean way to publish is Trusted Publishing (OIDC) from GitHub Actions — no API token stored in secrets. A workflow triggered on a GitHub Release builds and uploads; skip-existing makes re-runs idempotent so a re-published version stays green instead of failing.

on:
  release:
    types: [published]
permissions:
  id-token: write
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
      - run: python -m pip install build && python -m build
      - uses: pypa/gh-action-pypi-publish@release/v1

If you publish manually instead, use an account-scoped token the first time (the project doesn't exist yet), then switch to a project-scoped token and delete the account one.

6. CI across versions

A test matrix across Python 3.9–3.14 catches version-specific breakage. freelm runs pytest on every push; the release workflow only fires on a tag. Green CI is what gives you confidence to tag and ship.

7. Tag, release, verify

Bump the version, tag it (v0.2.2), and create a GitHub Release. After it's live, verify in a clean virtualenv:

python -m venv /tmp/verify && /tmp/verify/bin/pip install freelm
/tmp/verify/bin/python -c "import freelm; print(freelm.__version__)"

That clean-room install is the real test — it proves what users will get.

Frequently Asked Questions

Which build backend should I use in 2026? Hatchling is a great default — simple, fast, and standards-based. setuptools and PDM also work; the pyproject.toml interface is the same.

What is Trusted Publishing? A way for PyPI to accept uploads from a specific GitHub workflow via OIDC, so you never store a long-lived API token. Configure the publisher on PyPI once.

Do I need both a wheel and an sdist? Yes — the wheel is the fast install, the sdist is the buildable source. python -m build produces both.

How do I keep secrets out of git? Keep tokens in a gitignored .env or in GitHub Actions secrets, never in the repo. Verify with git check-ignore and scan history before going public.

How do I pick a package name? Choose something short and unique; check it's free on PyPI first (a 404 on pypi.org/project/<name> means available).


Written by Shihab Shahriar Antor — AI Engineer & Founder of Shahriar Labs. The package built in this walkthrough is freelm — source on GitHub.