Packaging and publishing with a professional mindset. The intermediate tutorials showed you how to structure a module and pin dependencies. Here we look at what it takes to share code – on PyPI, inside a company, or as a reusable library in several of your own projects.
Library, application, or both
A library exposes functions and classes other code imports. An application runs as a service or CLI. Mixing the two in one package without thinking about it leads to awkward installs and surprise imports at startup. Decide early which way the code is meant to be consumed.
Modern layout with pyproject.toml
pyproject.toml is the single config file build tools read. Keep version, dependencies, and entry points in one place.
# pyproject.toml (structure shown as comments in a code block)
# [project]
# name = "codeskill-utils"
# version = "0.1.0"
# description = "Small helpers for Codeskill examples"
# readme = "README.md"
# requires-python = ">=3.10"
# dependencies = ["httpx>=0.27"]
#
# [project.optional-dependencies]
# dev = ["pytest>=8.0", "ruff>=0.4"]
#
# [project.scripts]
# cs-report = "codeskill_utils.cli:main"
#
# [build-system]
# requires = ["hatchling"]
# build-backend = "hatchling.build"
The src/ layout keeps import tests honest – you install the package to use it, rather than accidentally importing from the repo root.
codeskill_utils/
├── pyproject.toml
├── README.md
├── src/codeskill_utils/
│ ├── __init__.py
│ └── slugify.py
└── tests/
└── test_slugify.py
Versioning and changelogs
Semantic versioning is a useful convention: bump the patch for fixes, minor for backwards-compatible features, major for breaking changes. A short CHANGELOG or GitHub release notes save future-you from archaeology. Even internal packages deserve a version string – “just use main” breaks the day someone needs to reproduce a build from six months ago.
Building and publishing to PyPI
Install build tooling in your virtual environment, build artefacts, upload with Twine. Use TestPyPI first unless you enjoy emailing strangers to yank a typo release.
# Terminal commands (run from project root):
# pip install build twine
# python -m build
# twine upload --repository testpypi dist/*
# twine upload dist/* # real PyPI when ready
Store API tokens in environment variables or your CI secret store. Never commit credentials to the repo. Enable two-factor auth on your PyPI account – bot uploads are a real nuisance.
Private indexes and git installs
Companies often run a private package index or install directly from Git tags:
# requirements snippet examples:
# codeskill-utils==0.3.1
# codeskill-utils @ git+https://github.com/you/codeskill-utils@v0.3.1
Pin versions in applications. Libraries declare compatible ranges. That split is easy to forget and painful when you forget it.
Quality gates before you publish
- README with install line and one working example
- Tests that run in CI
- License file if the code is public
requires-pythonset honestly- No secrets, no giant accidental data files in the sdist
Publishing is reversible in theory and annoying in practice. Treat the first public release as a contract – names, import paths, and behaviour people will depend on.

