Preparing a Library
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the Python Reference bibliography. |
Publishing starts before anything is built: the layout, the names and the metadata in pyproject.toml are what users and PyPI
see, and most of them cannot be changed for a release once it is uploaded. This page prepares the birdtally library for
publication. The examples ran on 2026-10-22 on Linux with CPython 3.14.6, build 1.6.1 and the backends hatchling 1.32.4,
setuptools 84.2.1 and poetry-core 2.5.0. The pyproject.toml keys themselves are described on
pyproject.toml; this page covers the decisions and the checks
around them. A Django-specific walk-through is on
Reusable Apps & Third-Party Packages.
Layout
| Layout | Shape | Effect |
|---|---|---|
src (recommended) |
|
The package is importable only after it is installed, so tests run against the installed code and a missing file shows up before release. Running the CLI from a checkout needs an (editable) install first. |
flat |
|
Importable from the project root without installing, which hides packaging mistakes. |
poetry new uses the src layout since 2.1; hatchling and setuptools find a src/<name> package on their own. birdtally:
pyproject.toml
README.md
LICENSE
CHANGELOG.md
src/birdtally/__init__.py
src/birdtally/cli.py
src/birdtally/tally.py
src/birdtally/py.typed
tests/test_tally.py
examples/survey.csv
Namespace packages let several distributions share one top-level import name (acme.birds, acme.maps). Use native
namespace packages (PEP 420): the shared directory has no __init__.py in any of the distributions. The older pkgutil and
pkg_resources styles are legacy; never mix styles across distributions of one namespace.
Names
-
The distribution name (
namein[project]) is what users install and what PyPI lists; the import name is what they import. Keep them the same unless there is a reason not to. PyPI and installers compare names after normalization (lowercase, runs of-,_and.become-), soBird_Tally,bird.tallyandbird-tallyare one project; see Name normalization. -
import-names(PEP 794, Core Metadata 2.5) declares the import names a distribution provides. hatchling 1.32.4 writes it:import-names = ["birdtally"]becomesImport-Name: birdtallyin aMetadata-Version: 2.5file. -
Choosing a name on PyPI: a name can be unavailable although no project shows it, because it clashes with the standard library, is too similar to an existing project (typosquatting protection), is prohibited (
requirements.txt), or is registered without releases. Abandoned names can be requested under PEP 541’s name-retention policy, which is slow and not guaranteed. Check the name before announcing it.
The metadata users see
[project]
name = "birdtally"
version = "0.1.0"
description = "Tally bird-survey sightings from CSV files."
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "The birdtally authors" }]
keywords = ["birds", "survey", "csv"]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Typing :: Typed",
]
[project.urls]
Homepage = "https://example.org/birdtally"
Changelog = "https://example.org/birdtally/changelog"
What the hatchling build wrote into the wheel’s METADATA:
Metadata-Version: 2.5
Name: birdtally
Version: 0.1.0
Summary: Tally bird-survey sightings from CSV files.
Project-URL: Homepage, https://example.org/birdtally
Project-URL: Changelog, https://example.org/birdtally/changelog
Author: The birdtally authors
License-Expression: MIT
License-File: LICENSE
Keywords: birds,csv,survey
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: pretty
Requires-Dist: rich>=13; extra == 'pretty'
Description-Content-Type: text/markdown
# birdtally
…
description and readme
description is the one-line summary at the top of the project page. readme becomes the long description, the body of the
page; its content type comes from the file extension: .md is text/markdown (GitHub-flavoured by default), .rst is
text/x-rst, anything else needs the table form with an explicit content-type. reStructuredText on PyPI is plain docutils:
Sphinx roles such as :py:func: make the upload fail to render (checked on
Checking & Testing Distributions).
Relative links and images in the README do not work on PyPI; use absolute URLs.
authors and maintainers
Both are lists of tables with a name, an email, or both. Their meaning is deliberately loose: authors may name the original
authors, the current maintainers or the owners. maintainers is for when the people who maintain the project now are not the
authors; the Core Metadata Maintainer field "should be omitted if it is identical to Author". A name alone becomes Author:
(or Maintainer:), as in birdtally's Author: The birdtally authors above; an entry with an email becomes Author-email:
(Maintainer-email:) in the form Name <email>, several entries separated by commas. A name must not contain a comma. These
fields only describe the project: they do not grant anyone rights on PyPI, where owners and maintainers are the
collaborators of the project (see
PyPI & TestPyPI Accounts & Tokens).
license and license-files (PEP 639)
Since PEP 639 (in the specification since December 2024), license is a single SPDX license expression ("MIT",
"Apache-2.0 OR MIT", "LicenseRef-Proprietary" for one that has no SPDX identifier), and license-files lists glob patterns
for the license texts to ship. Backends write License-Expression and one License-File per file, and put the files under
.dist-info/licenses/. The identifiers allowed are those of the SPDX License List version 3.17 or any later compatible
version (expression syntax: SPDX specification 2.2 or later), plus custom LicenseRef-… identifiers. The
SPDX list never removes identifiers, but marks some deprecated: tools should warn about those and PyPI may reject them. The
old table form (license = { file = "LICENSE" }) and the License :: classifiers are deprecated.
Minimum backend versions: setuptools 77.0.3, hatchling 1.27.0, flit-core 3.12, pdm-backend 2.4.0, poetry-core 2.2.0, uv_build
0.7.19. flit-core does not accept the WITH operator.
The specification says a pattern that matches no file is an error. The backends do not agree, so check the built wheel:
| Mistake | hatchling 1.32.4 | setuptools 84.2.1 | poetry-core 2.5.0 |
|---|---|---|---|
|
Builds silently, without any license file |
Builds, with a warning: |
Fails: |
|
Fails with a |
Fails: |
Builds with the legacy |
|
Builds (the specification lets tools choose) |
Fails: |
(not tried) |
Code vendored from elsewhere under another license makes the expression a combination ("MIT AND BSD-3-Clause"), and its
license text goes into license-files too (["LICENSE", "src/birdtally/_vendor/LICENSE.*"]).
Classifiers, URLs and requires-python
-
Classifiers describe the project for search and filtering on PyPI; they do not restrict installation. Only names from the
- official trove list are accepted. `Private
-
Do Not Upload` is always rejected by PyPI, which makes it a cheap safety net for internal packages.
-
URLs: the labels in
[project.urls]are free text, but PyPI recognises the well-known ones (homepage,source,download,changelog,releasenotes,documentation,issues,funding,security) after normalising them (lowercase, punctuation and spaces removed), shows icons for them, and marks a URL verified when it can prove the uploader controlled it at upload time (Trusted Publishing from that repository, or a link to the project’s own PyPI page). -
requires-pythonis checked by installers before anything else. Give a lower bound only: an upper bound such as<4makes resolvers back off to older releases on new Pythons rather than fail clearly.
Entry points: scripts and plugins
[project.scripts] (and [project.gui-scripts] for windowed programs on Windows) make installers create a command; birdtally
installs birdtally = "birdtally.cli:main". Any other group in [project.entry-points] is a plugin hook that other code can
discover through the installed metadata. Adding an exporter group:
[project.entry-points."birdtally.exporters"]
csv = "birdtally.tally:tally_by_site"
from importlib.metadata import entry_points
for ep in entry_points(group="birdtally.exporters"):
print(ep.name, "->", ep.value, ep.load())
csv -> birdtally.tally:tally_by_site <function tally_by_site at 0x7fb163aa5a60>
The wheel stores both in entry_points.txt ([console_scripts] and [birdtally.exporters]). Do not declare
[project.entry-points.console_scripts] yourself: backends must reject it in favour of [project.scripts]. Distributing the
CLI as an application is on
Distributing Applications & CLI Tools.
Files that must ship
-
py.typed: an empty marker file inside the package that tells type checkers the library is typed (PEP 561), paired with the- `Typing
-
Typed` classifier. The built wheel must contain it:
unzip -lon birdtally's wheel listsbirdtally/py.typedwith size 0.
-
Data files inside the package directory are included by hatchling and poetry-core; setuptools needs
package-dataor aMANIFEST.infor some cases. Read them withimportlib.resources, never with paths relative to__file__. -
CHANGELOG.mdand tests: whether they belong in the sdist is a backend setting (Poetry needs anincludeentry withformat = "sdist"). See Building sdists & Wheels for checking the contents.
The wheel of birdtally:
223 2020-02-02 00:00 birdtally/__init__.py
1089 2020-02-02 00:00 birdtally/cli.py
0 2020-02-02 00:00 birdtally/py.typed
979 2020-02-02 00:00 birdtally/tally.py
714 2020-02-02 00:00 birdtally-0.1.0.dist-info/METADATA
87 2020-02-02 00:00 birdtally-0.1.0.dist-info/WHEEL
49 2020-02-02 00:00 birdtally-0.1.0.dist-info/entry_points.txt
819 2020-02-02 00:00 birdtally-0.1.0.dist-info/licenses/LICENSE
702 2020-02-02 00:00 birdtally-0.1.0.dist-info/RECORD
pip and Poetry
Both flavours of birdtally carry the same [project] table; the Poetry flavour adds [tool.poetry] packages and an include
for the changelog (both files in full on
pyproject.toml).
poetry check validates the metadata before a build; on the pip side, the backend validates it at build time and twine check
after it.