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.
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.