← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 33: Packaging and Distribution Basics

Section 1

Packaging is the process of preparing code so that other people can install it, run it, and use it, without needing to understand its internal layout or to copy files by hand. A package, in this sense, is not just a folder of Python files. It is a folder of files plus metadata: a name, a version, a description, a list of dependencies, and instructions for how the code should be installed. That metadata is what turns a collection of files on your computer into a distributable unit that a tool can install reliably on another computer.

The need for packaging becomes clear as soon as you try to share code. If you simply send someone a folder, they must work out which file to run, which other files are needed, and which external packages must be installed first. They may place the folder in the wrong location, or miss a file, and the code may fail in confusing ways. A package solves those problems by defining a standard layout, a standard way to declare dependencies, and a standard way to install, so that the recipient's experience is predictable: install the package, and it works, or it reports a clear error about what is missing.

Packaging also helps you, as the author, to think clearly about your project. Deciding on a name, a version, and a description forces you to state what the project is for and who it is for. Listing dependencies forces you to know which external packages your code actually uses, rather than relying on packages that happen to be installed on your computer. That clarity benefits maintenance, because a project whose dependencies are explicit is easier to set up on a new computer, and easier for a new contributor to understand.

There are two related but distinct ideas that are often confused: a Python package, which is a folder containing modules that can be imported, and a distribution package, which is a file built from a project that can be installed. A project may contain one or more importable packages, and it produces distribution files, such as a source archive and a wheel, which are the files that installers actually use. Understanding that distinction helps you read packaging documentation, which uses both terms, and it helps you organise a project so that its importable code and its distribution metadata are in the right places.

Finally, packaging is a skill that grows with practice. A first package may be simple, with a single module, a short description, and no dependencies. Later packages may include command-line entry points, data files, documentation, and tests. Starting simple, and adding complexity only as needed, is the right approach, just as it is for programming generally. This chapter will guide you through that progression, from a first package to the considerations involved in sharing it more widely.

Check: What is the difference between an importable Python package and a distribution package?

Section 2

A well-organised project layout is the foundation of packaging, because installers and tools expect to find files in conventional places. A common layout places the project's importable code in a folder named after the project, at the top level of the repository or project folder, alongside a metadata file, a readme, a licence file, and a folder for tests. That layout is easy to navigate: a new contributor can see at a glance where the code lives, where the tests live, and where the project's description lives, without needing to read documentation about the layout itself.

An alternative layout places the importable code inside a source folder, often called src, with the metadata and other files at the top level. That layout has an advantage: it ensures that tests run against the installed code, rather than against the code in the project folder by accident, because the code is not directly importable from the project root. For larger projects, that separation is valuable, because it catches packaging mistakes early, such as a missing file that would be present when running from the project folder but absent when installed. For small projects, either layout works, and consistency within a project matters more than which layout you choose.

Naming is part of layout too. The folder containing importable code should have a name that is a valid Python identifier, usually lower-case, without spaces or hyphens, because it will be used in import statements. The distribution name, which is the name users type when installing, may differ slightly, for example by using hyphens where the import name uses underscores, but keeping the two names close reduces confusion. A project whose import name and distribution name are wildly different will puzzle users, who may install one name and try to import another, and wonder why it does not work.

Other files in a project layout serve distinct purposes. A readme explains the project to humans. A licence file states the terms under which others may use the code. A metadata file declares the project's name, version, dependencies, and other information for installers. A tests folder contains tests that verify the code works. Keeping each of those in its conventional place, with a conventional name, means that tools and humans can find them without special instructions, which is the point of conventions: they reduce the need for explanation.

Finally, a project layout should exclude files that should not be distributed, such as personal notes, temporary files, or credentials. Tools that build distribution files can be told which files to include and exclude, but the simplest safeguard is to keep the project folder tidy, with only files that belong to the project. A tidy project is easier to package, easier to share, and easier to understand, and tidiness is a habit worth cultivating from the first package onwards.

Check: Why does a src layout help catch packaging mistakes that a flat layout might hide?

Section 3

Metadata is the information about a project that installers, users, and tools need, and it lives in a dedicated file in modern Python packaging. That file declares the project's name, its version, a short description, the authors, the licence, and the Python versions it supports. It also declares dependencies: the external packages the project needs, and, optionally, extra sets of dependencies for particular purposes, such as testing or documentation. Writing metadata carefully is important, because it is how your project presents itself to the world, and how tools decide whether it can be installed in a given environment.

The name in metadata is the distribution name, used when installing. It should be descriptive, memorable, and, if the project will be shared publicly, unique enough not to clash with an existing project. Checking whether a name is already taken, before committing to it, saves the frustration of building a project around a name that cannot be used publicly. For private projects, uniqueness matters less, but a clear name still helps colleagues understand what the project does, and helps avoid confusion with other internal projects.

Versioning is another key part of metadata. A version is a string, usually composed of numbers separated by dots, that increases as the project changes. Users and tools use versions to decide whether an update is available, and whether it is safe to install. A version scheme that increases consistently, and that signals the scale of change, as discussed in the previous chapter, helps users judge updates. Changing a version for every release, and never reusing a version for different code, are basic rules that prevent confusion about which code a particular version refers to.

Dependencies in metadata should list only what the project actually needs, with version constraints that are as loose as safety allows. A constraint that is too strict, such as requiring an exact version of a dependency, may prevent installation alongside other projects that need a different version. A constraint that is too loose, such as allowing any version, may allow installation with a version that breaks the project. Finding the right balance, usually by allowing a range of versions that you have tested or that are known to be compatible, is a judgement that improves with experience and with testing.

Finally, metadata should include a longer description, often taken from the readme, so that users browsing a package index can understand the project without installing it. That description, together with the short description, the licence, and links to documentation and source code, forms the project's public face. Writing it for a new user, in plain language, rather than for yourself, increases the chance that the project will be found, understood, and used correctly.

Check: Why should dependency version constraints be neither too strict nor too loose?

Section 4

A version number is a promise, of sorts, about how much has changed since the previous version, and following a consistent scheme makes that promise meaningful. The most widely used scheme for Python projects uses three numbers: major, minor, and patch. A change in the patch number signals a small fix that should not break existing use. A change in the minor number signals a new feature that should remain compatible with existing use. A change in the major number signals a change that may break existing use, such as removing a feature or changing an interface. That scheme, often called semantic versioning, is a convention, not a rule enforced by tools, but following it helps users judge updates.

For a small personal project, strict adherence to that scheme may seem unnecessary, and a simpler scheme, such as increasing a single number for each release, may suffice. That is acceptable, as long as the scheme is consistent and documented, so that users know what to expect. What matters most is not which scheme you choose, but that versions always increase, that a version is never reused for different code, and that changes are documented, so that users can see what changed between versions. Those basic practices prevent the most common versioning confusions, regardless of the scheme.

Pre-release versions, such as versions for testing before a full release, have their own conventions, often involving labels such as alpha, beta, or release candidate, appended to the version number. Those labels signal that the version is not yet stable, and that users should expect changes before the final release. Using pre-release labels for test versions, rather than releasing them as stable versions, protects users who only want stable code, while still allowing enthusiastic testers to try new features and report problems. That separation between testing and stable releases is valuable for any project with users beyond its author.

Version numbers also interact with dependency constraints. When another project depends on yours, it will usually allow a range of your versions, based on the assumption that minor and patch changes will not break it. If you break that assumption, by making a breaking change in a minor version, you may break dependent projects that trusted your versioning. Following your chosen scheme consistently is therefore not just a courtesy; it is part of maintaining compatibility, as discussed in the previous chapter, and it affects users beyond those who install your project directly.

Finally, the version should be defined in one place, and that place should be the metadata file, with code that needs the version reading it from there, rather than duplicating it. Duplicated versions drift apart, as one is updated and another is forgotten, leading to a program that reports a different version from the one it was installed as. A single source of truth for the version, as for other metadata, prevents that inconsistency and makes releases simpler and more reliable.

Check: What does each number in a major, minor, patch version signal about the change?

Section 5

Dependencies are the external packages a project needs to work, and declaring them correctly is one of the most important parts of packaging. When a user installs your project, the installer reads its dependencies and installs them too, so that the project works without the user needing to know what it needs. If a dependency is missing from the declaration, the project may install successfully but fail when run, with an error about a missing module, which is confusing and frustrating. If a dependency is listed that the project does not actually use, installation becomes heavier and slower than necessary, and it may introduce conflicts with other projects.

Identifying dependencies accurately requires knowing what your code actually imports. A project that imports a package in its code needs that package as a dependency, unless the package is part of Python's standard library, in which case it is available wherever Python is installed and should not be listed. A project that uses a package only in its tests, or only for building documentation, should list that package separately, as an optional or development dependency, rather than as a core dependency, so that users who only want to run the project do not need to install test tools. That distinction keeps installations lean and focused.

Version constraints for dependencies, discussed in the previous section, should reflect what you know. If you have tested your project with a range of versions of a dependency, you can allow that range. If you know that a particular version breaks your project, you should exclude it. If you have only tested with one version, allowing a broad range is a guess, and it may lead to a broken installation for a user who gets an untested version. Testing with the versions you allow, at least for major versions, turns a guess into evidence, and it is worth doing for dependencies that are central to your project's function.

Optional dependencies, sometimes called extras, let users install additional features only if they want them. For example, a project might offer an extra for a particular output format that requires an additional package, or for a feature that most users do not need. Users who want that feature install the project with the extra named, and the installer adds the extra dependencies. Users who do not want it install only the core project, keeping their environment lighter. Extras are a good way to offer optional features without burdening every user with every dependency.

Finally, dependencies should be reviewed periodically. A project may stop using a package, in which case the dependency should be removed, or a package may become unmaintained, in which case an alternative should be considered. A dependency list that is not reviewed accumulates cruft, and it may eventually prevent installation, if a listed package becomes unavailable or conflicts with other packages. Regular review, as part of maintenance, keeps a project's dependencies accurate, minimal, and reliable.

Check: Why should test-only packages be listed separately from core dependencies?

Section 6

There are two main kinds of distribution files that a project produces: source distributions and wheels. A source distribution, often called an sdist, is an archive containing the project's source code, its metadata, and other files needed to build and install it. When a user installs from a source distribution, the installer builds the project on their computer, which requires build tools and may take time, but it ensures that the project is built for their specific environment. Source distributions are the most complete form of a project, because they contain everything needed to rebuild it.

A wheel is a pre-built distribution file, in a standard format, that can be installed quickly without building. Wheels contain the project's code, already organised for installation, plus metadata, but they do not contain the full source tree in the same way as a source distribution. Installing from a wheel is faster and more reliable than installing from source, because it does not require build tools on the user's computer, and because the wheel was built and tested by the project's author. For most users, installing from a wheel is the preferred experience, and most projects should provide wheels as well as source distributions.

The distinction matters for projects with different kinds of code. A pure Python project, which contains only Python code, can produce a wheel that works on any platform, because Python code is portable. A project that includes code in another language, which must be compiled for a specific platform, needs different wheels for different platforms, or must rely on source distributions that compile on installation. For the pure Python projects that are the focus of this chapter, a single, universal wheel is usually sufficient, and it simplifies distribution greatly.

Building distribution files is done with build tools, which read the project's metadata and layout, and produce the sdist and wheel files in a standard output folder. Those files can then be inspected, tested by installing them in a clean environment, and shared, either privately or publicly. Testing a built distribution, rather than assuming it works because the source code works, is important, because packaging mistakes, such as a missing file or an incorrect metadata entry, only become visible when the distribution is installed elsewhere. A distribution that has been tested in a clean environment is a distribution that can be trusted.

Finally, distribution files should be versioned consistently with the project's metadata, so that a user can tell which version a file corresponds to. A wheel or sdist filename includes the project's name and version, which helps users and tools identify it. Keeping built files organised, and not mixing files from different versions, prevents confusion when sharing or uploading distributions, and it makes it easy to find and remove old versions when they are no longer needed.

Check: Why is installing from a wheel usually faster and more reliable than installing from a source distribution?

Section 7

Entry points are how a packaged command-line application gets its command name. When a project defines an entry point, it tells the installer that a particular command, typed by the user, should run a particular function in the project's code. For example, a project might define a command called notes that runs the main function of its notes module. When the project is installed, the installer creates a small launcher for that command, so that the user can type notes in a terminal and the program runs, without needing to know which file contains the main function or how to invoke Python with it.

Entry points are a packaging feature, not an argparse feature, but they connect directly to the previous chapter's design. A program with a clean main function, as discussed there, is easy to expose as an entry point, because the entry point simply names that function. A program whose main logic is tangled with top-level code, or that only works when run as a specific file, is harder to package, because there is no clear function for the entry point to call. Designing for a clean main function therefore pays off again at packaging time, which is a good reason to adopt that structure early, even for scripts that are not yet packaged.

Defining an entry point involves stating the command name and the function it should call, in the project's metadata, using a standard format. The command name should be chosen carefully, as discussed in the previous chapter, because it is how users will invoke the program, and because it may conflict with other programs' commands. The function should be importable from the project's code, which means that the project's layout and import names must be correct. Testing an entry point, by installing the project in a clean environment and running the command, confirms that both the name and the function are correct.

Entry points are not only for command-line applications. They can also be used to expose plugins, or to register components that other programs can discover, but those uses are beyond the scope of this chapter. For the purposes of packaging a command-line tool, the key idea is simple: an entry point turns a function into a command, and that command is what users type. That transformation, from code to command, is what makes a packaged tool feel like a real program, rather than a script that must be run with explicit Python invocation.

Finally, entry points should be documented. A readme that shows the command name, with example commands, helps users who install the project to start using it immediately, without needing to guess the command or to look inside the package for its main function. Documentation that includes the command name, alongside installation instructions, completes the packaging story, by connecting the act of installing to the act of using.

Check: How does an entry point turn a Python function into a command a user can type?

Section 8

Installing a package locally, for testing, is an essential step before sharing it, and it should be done in a clean environment, such as a virtual environment, so that the test reflects what a new user will experience. A virtual environment is an isolated Python environment, with its own installed packages, separate from the system Python and from other projects. Creating one for testing, installing your package into it, and running its commands or importing its modules, confirms that the package is complete and correct, without the risk that a missing dependency is hidden because it happens to be installed in your main environment.

The process for local installation is straightforward in concept: build the distribution files, create a virtual environment, install the distribution into that environment, and test it. In practice, there are tools that combine building and installing for development, allowing you to install a project in editable mode, where changes to the source code are immediately reflected in the installed package, without reinstalling after every change. Editable installation is convenient during development, because it lets you test changes quickly, but it is not a substitute for testing a real, built distribution, because editable mode does not exercise the packaging steps that a real installation does.

Testing in a clean environment catches a specific class of packaging mistakes: missing files, missing dependencies, and incorrect metadata. For example, a project that imports a package but does not declare it as a dependency will work in your development environment, where that package is installed for other reasons, but will fail in a clean environment, where it is not. Similarly, a project that reads a data file that was not included in the distribution will work from its source folder, where the file exists, but will fail when installed, where the file is absent. Clean-environment testing reveals those mistakes before users do.

It is also useful to test installation on a different computer or operating system, if possible, especially for projects that will be shared widely. Path handling, file permissions, and available tools differ across systems, and a package that installs cleanly on one system may fail on another. Even testing on a second virtual environment, with a different Python version, can reveal compatibility problems, such as code that uses a feature not available in an older version. Those tests are part of packaging quality, not just programming quality.

Finally, local installation testing should include uninstallation, to confirm that the package can be removed cleanly, without leaving stray files or breaking other packages. A package that installs cleanly but cannot be uninstalled cleanly is a poor citizen in a user's environment, and it may cause problems when the user later tries to install a different version. Testing the full cycle, install, use, and uninstall, in a clean environment, gives confidence that a package is ready to share.

Check: Why is testing in a clean virtual environment more revealing than testing in your development environment?

Section 9

A readme is the first document most users will read about a project, and it serves both human and packaging purposes. For humans, it explains what the project does, why it is useful, how to install it, and how to use it, with examples. For packaging, its contents often become the project's long description in metadata, which is shown to users browsing a package index. Writing a good readme is therefore both a documentation task and a packaging task, and it deserves care proportional to its importance as the project's public face.

A good readme starts with a clear statement of purpose, in a sentence or two, so that a reader can decide within seconds whether the project is relevant to them. It then shows installation instructions, which should be as simple as possible, ideally a single command, and a first example that succeeds with minimal setup. After that, it can show more advanced examples, explain key options or features, and link to fuller documentation, if any exists. Ordering information from essential to advanced helps readers find what they need quickly, without wading through details they do not yet care about.

Examples in a readme should be tested, as discussed in the previous chapter, because an example that does not work undermines trust in the project as a whole. A readme that shows a command and its expected output, and where that command actually produces that output when run, gives readers confidence that the project works as described. Conversely, a readme with broken examples suggests that the project is unmaintained or untested, even if its code is sound. Keeping examples accurate is therefore part of maintaining a package, not just part of writing its readme.

A readme should also state the project's requirements, such as the Python versions it supports, and any major dependencies or system requirements, so that users can judge compatibility before installing. It should state the project's licence, or link to the licence file, so that users know the terms under which they may use the code. And it should explain how to report problems or contribute, if the project welcomes contributions, by pointing to an issue tracker or a contribution guide. Those sections, while less exciting than examples, are what make a project usable and trustworthy in practice.

Finally, a readme should be concise. A very long readme, covering every detail, is less useful than a short readme that covers the essentials and links to fuller documentation for details. Readers have limited time, and a readme that respects that time, by being clear, structured, and focused on getting started, is more likely to be read and followed. Brevity, combined with completeness for the essentials, is the goal for a good readme.

Check: Why does a readme serve both human readers and packaging tools?

Section 10

Licences state the terms under which others may use, modify, and share your code, and choosing one is an important part of packaging, especially for projects that will be shared publicly. Without a licence, others have no legal permission to use your code beyond what copyright law allows by default, which, in most places, is very little. That means that a project without a licence, even if its code is publicly visible, cannot safely be used by others, because they have no clear permission to do so. Adding a licence file removes that uncertainty, and it signals that the project is intended to be shared.

Choosing a licence involves thinking about how you want others to use your code. Some licences are permissive, allowing others to use the code in almost any way, including in closed, commercial projects, provided they include the licence and attribution. Others are copyleft licences, which require that modified versions of the code, when shared, are also shared under the same licence, preserving openness for downstream users. Neither approach is universally better; the choice depends on your goals for the project, and on the norms of the community you hope to join. For a learning project, a permissive licence is often a simple, welcoming choice, but the decision should be deliberate, not accidental.

A licence file should be included in the project, with its conventional name, so that tools and users can find it easily. The metadata should also declare the licence, so that package indexes can show it and users can filter by it. That dual declaration, in a file and in metadata, ensures that the licence travels with the project, whether it is viewed as source code or as an installed package. A licence that is only mentioned in a readme, or only in metadata, is less reliably found than one that is in both places, in the conventional locations.

It is also important to respect the licences of dependencies. If your project uses a package under a copyleft licence, your project's licence choices may be constrained, because the combined work must comply with that licence's terms. Checking dependencies' licences, especially for projects that will be distributed, is part of responsible packaging, and it is a topic that becomes more important as projects grow and gain users. For small, personal projects, this check may be simple, but it should not be ignored entirely, because licence incompatibilities can prevent distribution.

Finally, if you are unsure which licence to choose, seek guidance from a trusted source, such as a licence guide or a mentor, rather than copying a licence file without understanding it. A licence is a legal statement, and choosing one without understanding its terms can lead to unintended consequences, such as restricting use more than you intended, or permitting use you did not intend. Taking a little time to understand the main licence families, and to choose one that matches your intentions, is a worthwhile investment in a project you plan to share.

Check: Why does a project without a licence create uncertainty for potential users?

Section 11

A build backend is the tool that turns a project's source and metadata into distribution files, and understanding its role helps you make sense of packaging configuration. In modern Python packaging, the metadata file declares which build backend the project uses, and the build tool invokes that backend to produce a source distribution and a wheel. Different backends offer different features and conventions, but they all perform the same core job: reading metadata, gathering the project's files, and producing standard distribution files that installers can use. Choosing a backend is therefore a decision about tooling and conventions, not about the project's code itself.

For many projects, the default or most widely used backend is a safe choice, because it is well documented, well tested, and familiar to other developers who may contribute to the project. Using a widely used backend means that help, examples, and troubleshooting advice are easy to find, and that other tools, such as installers and package indexes, will handle your distributions correctly. Choosing an unusual backend for a simple project adds complexity without benefit, while choosing a backend with specific features, such as support for a particular layout or for compiled code, may be justified for a project that needs those features. The choice should be proportionate to the project's needs.

The metadata file's build declaration is short: it names the backend and any build-time dependencies it needs. Those build-time dependencies are distinct from the project's run-time dependencies, because they are only needed when building the project, not when running it. Keeping that distinction clear prevents build tools from being installed unnecessarily on users' computers, and it makes the project's requirements easier to understand. A project that confuses build-time and run-time dependencies may install correctly but fail to build, or build correctly but install unnecessary packages, both of which are avoidable with a clear declaration.

It is worth noting that build backends evolve, and that packaging standards change over time. A project that was packaged a few years ago may use an older configuration style that is now discouraged, even though it still works. For a new project, following current, standard practices, as documented in up-to-date packaging guides, is the best approach, because it ensures that the project will work with current tools and will be understandable to other developers. For an older project, updating its packaging configuration to current standards is a worthwhile maintenance task, though it should be done carefully and tested in a clean environment.

Finally, you do not need to understand every detail of how a build backend works to package a project successfully. You need to know how to declare it, how to invoke a build, and how to test the resulting distributions, as discussed in earlier sections. The backend handles the details of gathering files, generating metadata for the distribution, and producing the archive formats. Trusting the backend to do its job, while verifying its output by testing, is a good division of labour, and it lets you focus on your project's code and documentation, rather than on the mechanics of building.

Check: What is the role of a build backend in Python packaging?

Section 12

Many projects include files other than Python code, such as templates, sample data, configuration examples, or documentation, that the code needs at run time. Those files are often called data files or package data, and including them correctly in a distribution is important, because a project that works from its source folder, where those files are present, may fail when installed, where they are absent, if they were not included in the distribution. Understanding how to declare data files ensures that an installed project has everything it needs, not just its Python modules.

The key distinction is between files inside the importable package folder and files outside it. Files inside the package folder, alongside the Python modules, are usually treated as package data, and they can be included by declaring them in metadata, either by pattern, such as all files with a certain extension, or by listing them explicitly. Files outside the package folder, such as documentation or examples at the project root, are generally not needed at run time, and they are not included in a wheel, though they may be included in a source distribution for completeness. Knowing which files are needed at run time, and where they live, guides how to declare them.

Accessing data files at run time also requires care. Code that opens a data file by a relative path, assuming it is run from the project folder, will fail when installed, because the installed location is different. Modern Python provides tools for locating package data reliably, regardless of where the package is installed, by asking the package system for the file's location rather than assuming a path. Using those tools, rather than hard-coded relative paths, makes code robust to installation layout, and it is a habit worth adopting for any project that includes data files.

Testing data file inclusion, like testing other packaging aspects, requires a clean-environment installation. A test that runs code from the source folder will find data files even if they were not declared for distribution, because they are present in the source tree. A test that runs installed code, in a clean environment, will only find data files that were correctly included, so it reveals missing declarations. That test is therefore an essential part of packaging quality for projects with data files, and it should be performed before sharing a distribution.

Finally, keep data files minimal and purposeful. Including large, unnecessary files in a distribution makes it larger and slower to install, for no benefit. Including only the files the code actually needs, and documenting any large files that are required, keeps distributions lean and installations fast. If a project needs large data, such as a dataset, it may be better to download that data separately, on first use, rather than bundling it into the distribution, but that design decision should be documented clearly, so that users understand what will happen when they first run the program.

Check: Why might a project work from its source folder but fail when installed, regarding data files?

Section 13

Not every file in a project folder should be included in a distribution. Temporary files, personal notes, test data that is only used during development, and files containing secrets should be excluded, both to keep distributions lean and to avoid sharing information that should remain private. Modern packaging tools include, by default, the files that are needed: Python modules, metadata, and declared data files, and they exclude many common development files automatically. However, relying solely on defaults is risky, because a file that should be excluded may still be included if it matches a pattern, or a file that should be included may be excluded if it is in an unusual location.

Understanding what is included in a distribution is therefore part of packaging. After building a distribution, you can inspect its contents, by listing the files in the archive, and check that it contains what it should and nothing it should not. That inspection takes a few minutes, but it catches mistakes such as an accidentally included private file, or a missing data file, before the distribution is shared. It is a simple quality check, analogous to reading a dry-run plan before renaming files, as discussed in earlier chapters, and it should become a habit for every release.

For source distributions, inclusion rules can be more complex, because a source distribution is intended to contain everything needed to rebuild the project, including files that are not needed at run time, such as tests and documentation, but excluding files that are purely personal or temporary. Tools provide ways to declare additional files to include, and patterns to exclude, and those declarations live in the project's configuration. Learning the inclusion rules for your chosen build backend, and testing the resulting source distribution by building from it in a clean environment, ensures that the source distribution is complete and correct.

Secrets deserve special attention in this context. A file containing a password, a token, or a private key should never be in a project folder that might be packaged, and it should certainly never be included in a distribution. The safest practice is to keep secrets outside the project folder entirely, in a location that packaging tools will not see, and to read them from environment variables or a protected configuration, as discussed in the security chapter. If a secret was ever committed to a project folder, removing it from the folder is not enough, because it may remain in version history or in a previously built distribution, and that history needs careful handling, which is beyond this chapter but worth being aware of.

Finally, exclusion is also about clarity. A distribution that contains only the files needed to run the project is easier for users to understand, because they are not distracted by development files, notes, or experiments. A lean distribution installs faster, takes less space, and presents a cleaner face to the world. Keeping a project folder tidy, and inspecting distributions before sharing them, are simple habits that produce lean, trustworthy distributions.

Check: Why should you inspect a built distribution's contents before sharing it?

Section 14

Testing a package's imports is a simple but revealing check: after installing the package in a clean environment, can you import its modules, and do they work as expected? That check catches a range of packaging mistakes, such as a missing module file, an incorrect package name, a circular import that only manifests when imported in a certain order, or a dependency that was not declared and therefore is not installed. It is a check that should be performed for every release, because it verifies the most basic promise of a package: that it can be imported and used after installation.

Import testing should be done from a folder other than the project folder, to ensure that the test imports the installed package, not the source code in the project folder. If you run an import test from inside the project folder, Python may find the source code first, and the test will pass even if the installed package is broken or missing. Running the test from a temporary folder, or from a different location, ensures that the import resolves to the installed package, which is what a user will experience. That detail, while small, is crucial for the test to be meaningful.

Beyond a simple import, it is useful to run a smoke test: a short script that imports the package and exercises its main features, with sample data, to confirm that they work after installation. A smoke test does not need to cover every feature in depth; that is the job of the project's full test suite, which should also be run, but it should cover the main entry points, such as the main function of a command-line tool, or the main classes of a library. A smoke test that passes gives confidence that the installed package is functional, not merely importable.

It is also worth testing imports with different Python versions, if the project claims to support them. A package that uses a feature available only in a newer Python version will fail to import on an older version, even if its metadata claims compatibility. Testing with the oldest supported version, in a clean environment, catches such incompatibilities before users do. That test is especially important for projects that support a range of versions, because it is easy to accidentally use a new feature while developing on a newer version.

Finally, import testing should be automated where possible, as part of a release checklist or a continuous integration setup, so that it is not forgotten under time pressure. A checklist that includes building, installing in a clean environment, importing, smoke testing, and uninstalling, as discussed earlier, turns packaging quality from a hope into a repeatable process. That process, applied consistently, is what makes releases reliable, even for small projects.

Check: Why should an import test be run from a folder other than the project folder?

Section 15

Publishing a package publicly means uploading its distribution files to a package index, where other users can discover and install them with a single command. The main public index for Python is PyPI, and it is the default source for the standard installer. Publishing to PyPI makes a project available to anyone, which is a significant step, because it means that the project will be used, judged, and potentially depended upon by strangers. It should therefore be done deliberately, after the packaging checks discussed in this chapter have been performed, and after considering whether the project is ready for public use.

Before publishing to the main index, it is wise to practise with a test index, which is a separate instance of the package index software, intended for testing uploads and installations without affecting the main index. Uploading to a test index, installing from it in a clean environment, and confirming that the package works, lets you practise the upload process and catch mistakes, such as incorrect metadata or missing files, in a low-stakes setting. That practice is especially valuable for a first publication, because the upload process involves authentication and configuration that are easier to learn through practice than through reading alone.

Authentication for publishing should be handled carefully. Package indexes use tokens or credentials to verify that an uploader is authorised to publish a particular project name. Those credentials should be kept secret, stored securely, and never committed to a project folder or shared in documentation. If a credential is ever exposed, it should be revoked and replaced promptly, because an exposed credential could allow someone else to publish malicious versions of your project under your name. That risk, while serious, is manageable with careful handling, and it is a good reason to use tokens with limited scope, where the index supports them, rather than broad credentials.

Once a project is published, its versions should be treated as immutable. Publishing a new version with the same version number as an existing one, to fix a mistake, is generally not allowed, and it would confuse users who have already installed the old version. Instead, a mistake should be fixed in a new version, with a higher version number, and the changelog should explain the fix. That discipline, of never reusing a version, is part of the versioning practices discussed earlier, and it becomes especially important after publication, because published versions may already be in use by others.

Finally, publishing is not the end of a project's life; it is the beginning of its public life. After publication, users may report problems, request features, or contribute improvements, and the project author should be prepared to respond, even if only to say that the project is a learning exercise with limited support. Setting expectations in the readme, about the project's status and support, helps users judge whether to depend on it. A published project with clear expectations is more useful than one that promises more than its author can deliver.

Check: Why is practising with a test package index wise before publishing to the main index?

Section 16

Not every project needs to be published publicly. Many projects are for private use within a team, a company, or a classroom, and distributing them privately is often simpler and more appropriate than public publication. Private distribution can take several forms: sharing a wheel file directly, hosting a private package index, or installing from a version-controlled repository. Each form has trade-offs in terms of ease, control, and maintenance, and the right choice depends on the project's audience and on the organisation's practices.

Sharing a wheel file directly is the simplest form of private distribution. You build the wheel, send it to colleagues, and they install it from the file. That approach works well for a small team and an occasional release, because it requires no infrastructure. Its drawbacks become apparent as the team or the release frequency grows: users must manually track which version they have, updates must be sent out again, and there is no central place to discover the latest version. For a project with frequent updates or many users, a more structured approach is needed.

A private package index solves those problems by providing a central place where private packages can be uploaded and installed, using the same tools as public packages, but with access restricted to authorised users. Setting up a private index requires some infrastructure, but it gives users a familiar installation experience, with version tracking and dependency resolution handled by standard tools. For organisations with many internal projects, a private index is a worthwhile investment, because it makes internal code as easy to install as public code, while keeping it private.

Installing from a version-controlled repository is another option, where users install directly from a repository address, at a particular commit or tag. That approach is convenient during development, because it lets users test a change before it is formally released, but it is less stable for production use, because a repository's history can change, and because it does not provide the same version guarantees as a package index. It is best suited to development and testing, rather than to stable distribution, unless the repository's tags are treated as immutable releases.

Whichever private distribution method you choose, the packaging quality practices from this chapter still apply: build distributions, test them in a clean environment, document installation, and version them consistently. Private distribution does not relax those standards; if anything, it makes them more important, because private users may have less tolerance for broken installations than public users, who can choose another project. A private package that installs cleanly and works as documented is a valuable team asset, while one that does not will quickly be abandoned in favour of ad-hoc copying of files, which defeats the purpose of packaging.

Check: What are the drawbacks of sharing a wheel file directly as a team and release frequency grow?

Section 17

Dependency management for a project, as opposed to for a single installation, involves deciding which versions of dependencies to allow, and ensuring that those versions work together. When a project declares dependencies with ranges, the installer chooses specific versions within those ranges, based on what is available and what other installed packages require. That choice can vary over time, as new versions of dependencies are released, which means that a project that installed cleanly last month might install with different dependency versions this month, and might behave differently as a result. Understanding that variability is key to managing dependencies reliably.

One way to manage variability is to record, for a particular environment, the exact versions that were installed and tested, in a lock file or a requirements file with pinned versions. That record allows another person, or a future you, to recreate the same environment exactly, by installing the pinned versions, rather than relying on the installer's choices at that moment. Lock files are particularly valuable for applications, where reproducibility matters more than flexibility, because the application is deployed as a whole, and its environment should be identical wherever it runs. For libraries, which will be installed alongside other projects, pinned versions are less appropriate, because they can conflict with other projects' requirements, and ranges are preferred.

Testing with dependency ranges, rather than only with pinned versions, is also important for libraries. A library author should test, at least occasionally, with the latest versions allowed by its ranges, to ensure that it still works as dependencies evolve. If a new version of a dependency breaks the library, the library's range should be updated to exclude the breaking version, or the library should be updated to work with it, and a new version released. That ongoing maintenance, of keeping ranges accurate as dependencies change, is part of a library's life, and it requires attention beyond the initial release.

Conflicts between dependencies are a common problem. Two projects may require different, incompatible versions of the same dependency, making it impossible to install both in the same environment. Resolving such conflicts may involve updating one project to allow a newer version, or using separate virtual environments for the two projects, so that each has its own dependencies. Virtual environments, discussed in earlier chapters, are the standard solution to conflicts, because they isolate projects from each other. Understanding that isolation, and using it deliberately, prevents many dependency headaches.

Finally, dependency management is a balance between stability and freshness. Pinning everything gives stability but risks falling behind, missing security fixes and improvements in dependencies. Allowing everything gives freshness but risks breakage from untested changes. A sensible middle path, of allowing ranges for libraries, pinning for deployed applications, and testing regularly with current versions, balances those risks, and it is a practice that improves with experience and with attention to the project's needs.

Check: Why are pinned dependency versions suitable for applications but less suitable for libraries?

Section 18

A changelog is a record of what changed in each version of a project, written for users, and it is an essential part of a well-maintained package. When a user considers updating to a new version, the changelog is how they judge whether the update is safe, whether it includes a feature they need, or whether it fixes a problem they encountered. Without a changelog, users must guess, or read code changes, which is time-consuming and unreliable. A changelog that is clear, concise, and organised by version therefore makes updates easier and safer for everyone.

A good changelog entry for a version lists its changes under headings such as added, changed, fixed, and removed, with each change described in plain language, focusing on what changed for users, rather than on internal code details. A change described as "fixed a bug where empty files were counted as errors" is useful; a change described as "refactored function X" is less useful, because it does not say what changed for the user. Writing changelog entries from the user's perspective, rather than the developer's, is a habit that makes changelogs genuinely helpful, rather than merely a list of commits.

Changelogs should be updated as part of making a release, not afterwards from memory. When you prepare a new version, gather the changes since the last version, write them in the changelog, and include the changelog in the distribution, so that users can read it without needing to visit a separate site. That discipline ensures that the changelog is accurate, because it is written while the changes are fresh, and it ensures that it is available wherever the project is available, including in the installed package or its repository.

Release process, more broadly, is the sequence of steps taken to produce a new version: updating the version number, updating the changelog, building distributions, testing them in a clean environment, and uploading or sharing them. Following the same sequence for every release, ideally from a checklist, reduces the chance of forgetting a step, such as testing or updating documentation, under time pressure. A checklist need not be formal; a short list in the project's readme or in a contributor guide is enough, as long as it is followed consistently. That consistency is what makes releases reliable, even for small projects.

Finally, a release should be announced, at least to its users, in a way appropriate to its audience. For a public project, that might mean updating its readme or its index page. For a private project, it might mean telling colleagues what changed and how to update. Announcing a release, with a link to its changelog, closes the loop between development and use, and it ensures that users know an update is available and whether they should install it. A release that is not announced may go unused, which wastes the work that went into it.

Check: Why should changelog entries be written from the user's perspective rather than the developer's?

Section 19

Maintaining a package after its first release involves responding to problems, updating dependencies, and releasing new versions, and it requires a different mindset from initial development. Initial development is about building features; maintenance is about preserving trust. Users who have installed a package expect it to continue working, to receive fixes for serious problems, and to be updated in a way that does not break their existing use without warning. Meeting those expectations, even for a small project, is what distinguishes a maintained package from an abandoned one.

Responding to problems starts with making it easy for users to report them, by providing a clear way to contact the author or to file an issue, and by stating what information is helpful in a report, such as the version used, the command run, and the error message received. When a problem is reported, reproducing it in a clean environment, with the reported version, is the first step to fixing it, just as reproducing a bug is the first step in debugging generally. A fix should then be tested, released as a new version, and noted in the changelog, closing the loop from report to release.

Updating dependencies is another maintenance task, as discussed earlier. Dependencies evolve, and a package that is not updated may eventually fail to install, because a dependency it requires is no longer available, or may become vulnerable, because a dependency has a known problem that was fixed in a later version. Regularly checking dependencies, testing with newer versions, and updating ranges or code as needed, keeps a package installable and safe. That checking need not be frequent for a small project, but it should not be neglected entirely, because neglect accumulates into a package that is hard to revive.

Deciding when to release a new version is also part of maintenance. A fix for a serious problem should be released promptly, as a patch version, so that users can benefit quickly. A new feature can wait for a minor version, grouped with other features, to avoid frequent releases that burden users with constant updates. A breaking change should be released as a major version, with clear notice, so that users can plan their update. Those decisions, guided by the versioning scheme discussed earlier, balance responsiveness with stability, and they show respect for users' time.

Finally, maintenance includes knowing when to retire a package. A project that is no longer needed, or that has been superseded by a better alternative, should be marked as such, with a note in its readme and, if public, in its index page, directing users to the alternative. Retiring a package gracefully, rather than simply abandoning it without explanation, helps users plan their migration, and it preserves the author's reputation as a responsible maintainer. Even retirement, done well, is part of good packaging practice.

Check: How does maintenance differ in mindset from initial development of a package?

Section 20

Common packaging mistakes are worth learning explicitly, because they are easy to make and easy to avoid once recognised. One common mistake is forgetting to declare a dependency, as discussed earlier, which leads to a package that works in development but fails when installed cleanly. Another is forgetting to include a data file, which leads to a similar failure, where code cannot find a file it needs. Both mistakes share a root cause: testing only in the development environment, where missing items are present by accident. Testing in a clean environment, as emphasised throughout this chapter, prevents both.

Another common mistake is using a project name that conflicts with an existing package, or that is confusingly similar to one. That mistake is easy to make when a name is chosen without checking, and it is hard to fix after publication, because the name becomes part of the project's identity. Checking a name before committing to it, and choosing a distinctive name, avoids that problem. Similarly, using an import name that conflicts with a standard library module, or with a widely used package, can cause import confusion, where the wrong module is imported, leading to errors that are hard to diagnose. Choosing import names carefully, and testing imports in a clean environment, prevents that confusion.

A third common mistake is hard-coding paths, versions, or settings in multiple places, leading to inconsistency when one is updated and another is not. That mistake, discussed in the context of versions, applies more broadly: any value that appears in several places should be defined once and referred to elsewhere. Single sources of truth, for versions, for paths, and for other shared values, prevent drift and make maintenance easier. Recognising duplication as a warning sign, and refactoring it into a single definition, is a habit that serves packaging as well as programming generally.

A fourth mistake is neglecting documentation, especially the readme and the changelog. A package without a readme is hard for users to evaluate, and a package without a changelog is hard for users to update safely. Both documents are part of the package, not optional extras, and they should be written and maintained with the same care as code. A package whose documentation is accurate and up to date is a package that users can trust, while one whose documentation is missing or stale will be viewed with suspicion, even if its code is excellent.

Finally, a common mistake is treating packaging as a one-time task, done once at the start and then forgotten. Packaging, like code, needs maintenance: dependencies need updating, metadata needs correcting, and distributions need rebuilding for new versions. A project whose packaging is maintained alongside its code remains installable and usable over time, while one whose packaging is neglected gradually becomes hard to install, even if its code still works. Treating packaging as an ongoing part of development, rather than a final hurdle, is the mindset that prevents most packaging mistakes.

Check: What root cause do missing dependencies and missing data files share as packaging mistakes?

Section 21

The metadata file used in modern Python packaging is a text file with a standard format, organised into sections that declare different aspects of a project. The first section declares the build system: which backend to use and which build-time dependencies it needs. The second section declares the project itself: its name, version, description, authors, licence, dependencies, and other information. Additional sections can declare optional dependencies, entry points, and tool-specific settings. Understanding that structure, at a high level, helps you read and write packaging configuration without needing to memorise every field.

The build system section is short but important, because it tells the build tool how to build the project. Without it, the build tool must guess, and guessing can lead to inconsistent results. Declaring the backend explicitly, and listing its build-time dependencies, ensures that the project builds the same way wherever it is built, as long as the same backend and dependencies are available. That reproducibility is a core goal of modern packaging, and it is why the build declaration is required, even for simple projects.

The project section is where most metadata lives. Its fields include the name and version, discussed earlier, a short description, a longer description, often read from the readme, the names and contact details of authors or maintainers, the licence, and the Python versions supported. Each field has a purpose: the name identifies the project, the version identifies the release, the description helps users evaluate it, the authors field gives credit and contact, the licence states terms of use, and the supported Python versions tell installers whether the project can be installed in a given environment. Filling in those fields completely, rather than leaving them blank, makes a project more discoverable and more trustworthy.

Dependencies are declared in the project section as a list, with each entry naming a package and, optionally, a version constraint. Optional dependencies are declared in a separate part of the same section, grouped by extra name, so that users can install a project with a particular extra by naming it. Entry points are declared in another section, mapping command names to functions, as discussed earlier. Tool-specific settings, such as how to find packages in the project layout, or which files to include as data, live in sections named for the tool or backend, keeping them separate from the standard project metadata.

Finally, the metadata file should be validated. Tools can check that the file is well-formed, that required fields are present, and that values are in the expected format, before a distribution is built. Validating early catches mistakes such as a misspelled field name, which would otherwise be silently ignored, leading to a distribution missing important metadata. That validation step, combined with building and testing in a clean environment, forms a quality gate for packaging, ensuring that what is declared is what is actually built and shared.

Check: What are the two main sections of a modern Python packaging metadata file?

Section 22

Choosing a build backend involves weighing familiarity, features, and community support, and for most projects the choice is less critical than getting the basics right. A backend that is widely used, well documented, and actively maintained is a safe choice, because it will handle standard layouts and metadata correctly, and because help will be available if problems arise. A backend that offers a particular feature, such as a different way of declaring packages or support for a particular kind of project, may be worth choosing if that feature matters to your project, but it should not be chosen for novelty alone, because novelty adds learning cost without necessarily adding value.

One way to think about backends is in terms of what they assume about your project. Some backends assume a conventional layout and require little configuration, which is ideal for a simple project that follows conventions. Others offer more configuration options, which is useful for a project with an unusual layout or special needs, but which also requires more understanding to configure correctly. Matching the backend's assumptions to your project's layout, rather than fighting them, leads to simpler configuration and fewer surprises. If a backend requires extensive configuration for a simple project, it may not be the right fit.

Community and ecosystem support also matter. A backend that is used by many projects will be tested with many different layouts, metadata, and environments, and its bugs will be found and fixed more quickly. It will also be supported by other tools, such as installers, package indexes, and continuous integration services, which may assume a particular backend or work best with it. Choosing a backend with broad support reduces the risk of encountering a tool incompatibility that is hard to resolve, especially for a project that will be shared publicly.

It is also possible to change backends later, if a project's needs change. Changing a backend involves updating the build declaration and any backend-specific configuration, and then rebuilding and testing distributions in a clean environment to confirm that the change did not alter the project's contents or metadata. That change should be made deliberately, with testing, rather than casually, because a backend change can affect what is included in a distribution, even if the project's code has not changed. Testing after a backend change, as after any packaging change, is essential.

Finally, remember that the backend is a tool, not the project itself. The project's value lies in its code, its documentation, and its usefulness to users, not in which backend built it. Spending a disproportionate amount of time choosing or configuring a backend, at the expense of writing clear code and documentation, is a poor use of effort. Choose a sensible, well-supported backend, configure it simply, test its output, and move on to the work that matters most: making the project itself good.

Check: Why is a widely used, well-supported build backend a safe choice for most projects?

Section 23

Namespace packages are a way to organise related packages under a shared top-level name, without requiring a single project to contain all of them. For example, several projects from the same organisation might all live under a common prefix, such as a company name, while each project is distributed and installed separately. When installed together, their modules appear under the same namespace, as if they were parts of a single larger package, even though they came from different distributions. That organisation can make a collection of related projects easier to discover and to import, because their names share a recognisable prefix.

Namespace packages are an advanced feature, and they are not needed for most small projects. A single project, with a single top-level package, is simpler to understand, to package, and to maintain, and it should be the default choice. Namespace packages become useful when a collection of projects grows large enough that a shared prefix helps users navigate it, or when different teams contribute parts of a larger system that should appear under a common name. In those cases, the added complexity of namespace packaging is justified by the organisational benefit, but it should not be adopted for a small project simply because it sounds sophisticated.

Implementing namespace packages requires care in layout and in metadata, because the absence of a traditional package initialisation file, or the presence of a particular declaration, changes how Python treats the namespace. Different build backends handle namespace packages in slightly different ways, so following the documentation for your chosen backend, and testing imports in a clean environment after installing multiple namespace parts together, is essential. A namespace package that works when only one part is installed, but fails when several parts are installed together, has not been tested thoroughly, and that failure will confuse users who install the full collection.

There are also alternative ways to achieve similar organisation without namespace packages, such as using a distinctive prefix in each package's name, without sharing a namespace. For example, projects named after an organisation, with a hyphen or underscore separating the organisation name from the project name, achieve recognisability without the complexity of namespaces. That simpler approach is often sufficient, and it avoids the subtleties of namespace handling. Choosing the simpler approach, unless there is a specific reason to need a true namespace, is consistent with the simplicity principle that runs through this book.

Finally, namespace packages illustrate a broader packaging lesson: features that solve organisational problems for large collections can add unnecessary complexity for small projects. Packaging decisions should be proportionate to a project's size and needs, and advanced features should be adopted when they solve a real problem, not in anticipation of a problem that may never arise. Starting simple, and adding complexity only when needed, keeps packaging manageable and projects maintainable.

Check: When might namespace packages be justified, despite their added complexity?

Section 24

Editable installation is a development workflow where a project is installed in a way that reflects changes to its source code immediately, without reinstalling after every edit. That workflow is convenient during development, because it lets you test changes quickly: you edit a file, run the program or its tests, and see the effect, without an intervening build and install step. For a developer working on a project daily, that immediacy saves time and reduces friction, and it is why editable installation is a standard part of Python development practice.

Editable installation works by creating a link, or a small shim, in the environment, pointing to the project's source code, rather than copying the code into the environment. When Python imports the project, it follows that link and loads the source code directly. That means that edits are visible immediately, but it also means that the installed project is not a true distribution: it depends on the source folder remaining in place, and it may include files that would not be in a real distribution. Editable installation is therefore a development tool, not a distribution method, and it should not be confused with installing a built wheel or source distribution.

The distinction matters for testing. A test run in editable mode may pass even if the project's packaging is broken, because the test imports source code directly, bypassing the packaging steps that would reveal a missing file or dependency. A test run against a real, built distribution, installed in a clean environment, exercises packaging as well as code, and it is the test that matters for users. Both kinds of testing are valuable, but they test different things, and relying only on editable-mode testing leaves packaging untested, which is a common source of the mistakes discussed earlier.

A good development workflow uses both modes appropriately: editable installation for day-to-day development and quick testing, and built-distribution installation in a clean environment for release testing, before sharing a new version. That workflow combines the speed of editable mode with the rigour of distribution testing, and it ensures that packaging problems are caught before they reach users, while development remains efficient. Documenting that workflow, for contributors as well as for yourself, helps new developers set up the project correctly and avoid testing only in editable mode by accident.

Finally, editable installation should be set up in a virtual environment, not in the system Python, to avoid polluting the system environment with development links and to keep projects isolated from each other. That isolation, discussed in earlier chapters, is standard practice for Python development, and it applies to editable installation as much as to regular installation. A clean, isolated development environment, with editable installation for the project under development, is a productive and safe setup for packaging work.

Check: Why does testing only in editable mode leave packaging untested?

Section 25

Supporting multiple Python versions is a packaging decision that affects code, metadata, and testing. A project declares, in its metadata, which Python versions it supports, and installers use that declaration to decide whether the project can be installed in a given environment. Declaring support for a version means promising that the project works on that version, which requires testing on it, not just assuming it works because it works on a newer version. That promise should be made carefully, based on actual testing, rather than on optimism.

The range of versions to support involves a trade-off. Supporting many versions, including older ones, makes a project available to more users, including those on older systems that cannot easily upgrade. However, it also constrains the code, because features available only in newer versions cannot be used, or must be used conditionally, with fallbacks for older versions. Supporting only recent versions allows the code to use newer features freely, and reduces testing burden, but it excludes users on older systems. Choosing a range that balances those factors, based on the project's audience and on the features it needs, is a deliberate decision, not a default.

Testing across versions can be done with virtual environments for each supported version, or with tools that automate testing across versions. For a small project, testing manually on the oldest and newest supported versions may be sufficient, because those are the extremes where incompatibilities are most likely to appear. For a larger project, automated testing across all supported versions, on each change, provides stronger assurance, and it catches incompatibilities introduced by a change that uses a new feature without realising it. That automated testing is part of continuous integration, discussed later in this chapter.

Code that supports multiple versions should avoid version-specific tricks where possible, by using features available in all supported versions, or by using compatibility patterns that are clear and documented. Conditional code that checks the Python version and behaves differently is sometimes necessary, but it adds complexity and makes testing harder, because each branch must be tested on the versions it targets. Minimising such conditional code, by choosing a version range that allows a consistent style, keeps the codebase simpler and more maintainable.

Finally, when dropping support for an older version, do so as a deliberate release decision, documented in the changelog, and preferably in a minor or major version, depending on the project's versioning scheme and on how many users are affected. Dropping support without notice can break users who relied on the old version, even if their code has not changed. Announcing the change, and giving users time to upgrade their Python version or to stay on an older project version, respects their investment and maintains trust in the project.

Check: Why should a claim to support a Python version be based on testing, not assumption?

Section 26

Distribution size matters, because larger distributions take longer to download and install, use more disk space, and may be harder to share, especially in environments with limited bandwidth. For a pure Python project, distribution size is usually small, because Python code is compact, but it can grow if the project includes large data files, images, documentation, or test data that are not needed at run time. Keeping distributions lean, by including only necessary files, as discussed earlier, is therefore both a packaging quality issue and a user experience issue.

Measuring distribution size is simple: after building, check the size of the wheel and source distribution files, and list their contents to see what contributes most to the size. That inspection often reveals surprises, such as a large test data file that was included by accident, or documentation images that are not needed at run time. Removing unnecessary files, or moving large data to a separate download, as discussed earlier, can reduce size dramatically, with no loss of functionality. That measurement and reduction, performed before release, is a quick win for users.

For projects that genuinely need large files, such as a dataset or a model, there are alternatives to bundling them into the distribution. The project could download the data on first use, from a documented location, with a clear message about what is being downloaded and why. It could offer the data as a separate, optional package, so that users who do not need it do not download it. Or it could document how users can supply their own data, in a documented format, rather than bundling a default dataset. Each alternative has trade-offs, but all of them keep the core distribution lean, which benefits users who do not need the large data.

Compression also affects distribution size, but it is handled automatically by distribution formats, which use standard compression. Authors do not need to compress files manually, and doing so can actually interfere with installation, because installers expect standard formats. The author's job is to control which files are included, not how they are compressed, and to trust the distribution format to handle compression efficiently. That division of labour, between author decisions about content and tool decisions about format, is a recurring theme in packaging.

Finally, distribution size should be considered in proportion to the project's value. A small utility that downloads a large dataset on installation may be perceived as wasteful, while a data-heavy project that is genuinely about that data may justify a larger size. Judging size by the project's purpose, and by the experience of users with limited bandwidth, leads to sensible decisions, rather than an absolute rule about size. A lean distribution, justified by its contents, is a sign of a thoughtfully packaged project.

Check: Why should large, optional data be kept out of a core distribution?

Section 27

Integrity and trust are concerns for any distributed software, because users who install a package are trusting its author and its distribution channel. Package indexes and installers provide some protections, such as checking that a downloaded file matches its expected hash, which is a fingerprint computed from the file's contents. If a file is corrupted or tampered with during download, its hash will not match, and the installer can refuse to install it, protecting the user from a damaged or altered package. That hash checking happens automatically for most installations, and it is a basic integrity safeguard that users benefit from without needing to understand it.

Authors can support integrity by ensuring that their distributions are built reproducibly, meaning that building the same source with the same tools produces the same distribution file, or at least a distribution with the same contents. Reproducible builds make it possible to verify that a distribution corresponds to a particular source version, which is valuable for auditing and for trust. While full reproducibility can be complex, involving timestamps and build environments, following standard build practices and avoiding non-deterministic steps in the build, such as including timestamps in generated files, moves a project towards reproducibility.

Signing distributions, where the author cryptographically signs a file to prove its origin, is a more advanced integrity measure, and its practices vary across ecosystems and over time. For the purposes of this chapter, the key idea is that signing, where used, provides a stronger guarantee than hash checking alone, because it ties a distribution to its author, not just to its contents. However, signing also requires key management, which, if done poorly, can create its own risks, such as a lost or compromised key. Authors should follow current, trusted guidance for their chosen index and tools, rather than inventing their own signing practices, and they should understand that signing is a complement to, not a replacement for, careful packaging and testing.

Users also play a role in integrity, by installing packages only from trusted sources, by checking a project's reputation, and by being cautious about packages with suspicious names or descriptions that mimic well-known projects. That caution, known as protection against typosquatting, where a malicious package uses a name similar to a popular one to trick users into installing it, is important, because package indexes are open to many authors. Authors can help by choosing distinctive names, as discussed earlier, and by documenting their project's official installation source, so that users can verify they are installing the right project.

Finally, integrity is not only about malicious tampering; it is also about accidental corruption, such as a truncated download or a disk error. Hash checking protects against those accidents as well, by detecting that a file is not as expected before it is installed. That protection, combined with careful authoring, distinctive naming, and trusted distribution channels, forms a layered approach to integrity, where no single measure is perfect, but together they make installing packages reasonably safe for most users.

Check: How does hash checking protect a user during package installation?

Section 28

Documentation packaging is the question of what documentation should be included in a distribution, and what should live elsewhere, such as on a website or in a repository. A wheel, which is for installation, does not need to contain full documentation, such as tutorials or detailed guides, because users who need that documentation can read it online or in the repository. Including full documentation in a wheel would bloat the distribution for no benefit, since installed users rarely read documentation from inside an installed package. A source distribution, by contrast, often includes documentation, because it is intended as a complete source tree, from which documentation can be built or read.

For most projects, the readme serves as the core documentation in the distribution, because it is shown on the package index and included in both source and wheel distributions, at least in summary form. Fuller documentation, such as a user guide, an application programming interface reference, or tutorials, usually lives in a docs folder in the repository, and is published separately, for example as a website, rather than being bundled into the installed package. That separation keeps distributions lean, while still making full documentation available to users who seek it out.

When documentation is published separately, it should be linked from the readme and from the package metadata, so that users can find it easily. Metadata fields for documentation, homepage, and source code links serve that purpose, by giving package indexes and tools a standard place to show those links. Filling in those fields, with correct, working links, is a small step that greatly improves discoverability, because users browsing a package index can click through to documentation, source code, and issue trackers without guessing addresses. Broken links, by contrast, undermine trust, so they should be checked periodically.

Building documentation, where it is more than a readme, often involves a documentation tool that converts source files, such as text files with markup, into a website or a set of pages. That build process is separate from packaging the code, and its output is usually not included in the code distribution. Keeping documentation building separate from code packaging avoids coupling the two, so that a documentation change does not require a new code release, and vice versa. That separation also allows documentation to be updated more frequently than code, which is often desirable, because documentation improvements do not need to wait for a code release.

Finally, documentation should be versioned alongside code, at least loosely, so that users can find documentation for the version they have installed, not only for the latest version. That versioning can be as simple as noting in the changelog which documentation changes correspond to which code version, or as structured as publishing documentation for each major version separately. For small projects, a single, current documentation set, with notes about version differences where they matter, is usually sufficient, and it is far better than no documentation at all.

Check: Why is full documentation usually published separately rather than bundled into a wheel?

Section 29

Continuous integration, often abbreviated as CI, is the practice of automatically building and testing a project on every change, using a service that runs in a clean environment. For packaging, CI is valuable because it automates the checks discussed in this chapter: building distributions, installing them in a clean environment, running imports and smoke tests, and running the full test suite. Automating those checks means that they are performed consistently, without relying on a developer remembering to run them, and that packaging mistakes are caught as soon as they are introduced, rather than at release time, when they are more costly to fix.

A basic CI setup for packaging might run on every change to the project's main branch, and on every proposed change, such as a pull request. It would create a clean environment, install the project's dependencies, run its tests, build its distributions, and install those distributions in a fresh environment to smoke test them. That sequence, while it sounds elaborate, can be expressed in a short configuration file, and many CI services provide templates for Python projects that cover most of it. Starting from such a template, and adapting it to the project's needs, is a practical way to adopt CI without needing to design it from scratch.

CI also helps with testing across Python versions and operating systems, as discussed earlier. A CI configuration can specify a matrix of versions and systems, and the service will run the project's tests on each combination, reporting which ones pass and which fail. That matrix testing, which would be tedious to do manually, becomes automatic, and it gives confidence that a project works across its claimed support range. For a project that supports several versions, CI matrix testing is one of the most valuable tools available, because it catches version-specific problems early and consistently.

There are limits to CI. It cannot replace thoughtful design, clear documentation, or manual testing with realistic data, because automated tests only check what they were written to check. A CI setup that passes all its tests can still produce a package with confusing help text, or with a readme that is out of date, because those are not easily tested automatically. CI should therefore be seen as a safety net for packaging and code quality, not as a guarantee of overall quality. Human review, of code, documentation, and interface, remains essential, even for a project with excellent CI.

Finally, CI configuration itself needs maintenance. As a project evolves, its CI configuration may need updating, for example to add a new Python version, to change a build step, or to update a service's configuration format. Keeping CI configuration in the project's repository, alongside code, ensures that it is versioned and reviewed like other changes, and that a new contributor can see how the project is tested. A CI setup that is maintained alongside the project is a valuable asset; one that is neglected and starts failing for unrelated reasons becomes a burden that developers learn to ignore, which defeats its purpose.

Check: How does continuous integration catch packaging mistakes earlier than release-time testing?

Section 30

Packaging a library differs from packaging an application, and understanding that difference helps you make appropriate choices for each kind. A library is code that other programs import and use, such as a set of functions for handling dates or for parsing a format. An application is a program that users run directly, such as a command-line tool or a graphical program. Libraries are usually installed alongside other libraries, in a shared environment, so their dependencies must be flexible, to avoid conflicts with other libraries' dependencies. Applications are usually deployed as a whole, in their own environment, so their dependencies can be pinned, to ensure reproducibility, as discussed earlier.

For a library, the importable interface is the product. Its package name, its module structure, and its public functions are what users depend on, and changing them is a breaking change that should be handled with care, as discussed in the context of versioning. Documentation for a library should therefore focus on its interface: what functions and classes it provides, what arguments they take, and what they return, with examples of use. A library's entry points, if any, are secondary, because most users will import it rather than run it as a command.

For an application, the command-line interface, or graphical interface, is the product. Its command name, its options, and its behaviour when run are what users depend on, and changing them is a breaking change, as discussed in the previous chapter. Documentation for an application should therefore focus on installation, on running it, and on its commands and options, with examples of typical use. An application's importable modules are secondary, because most users will run it rather than import it, though they may still be useful for testing or for embedding.

Dependencies differ too, as noted above. A library should declare dependencies with ranges that allow flexibility, so that it can coexist with other libraries. An application should, for deployment, record exact versions, so that its environment can be reproduced exactly. That difference is not about one approach being better in general; it is about matching the dependency strategy to the project's role. A project that is both a library and an application, offering both an importable interface and a command, needs to balance those strategies, usually by declaring flexible ranges in its metadata, while providing a pinned requirements file for deployment, as a separate artefact.

Finally, testing differs in emphasis. A library's tests focus on its functions and their edge cases, ensuring that its interface behaves correctly for a range of inputs. An application's tests focus on its commands, its output, and its exit codes, ensuring that it behaves correctly when run by users and scripts. Both kinds of testing are important, and a project that is both should do both, but recognising which kind matters most for a given project helps prioritise testing effort. Packaging, in both cases, serves the same goal: making the project easy to install and reliable to use, whether it is imported or run.

Check: Why should a library's dependencies be flexible while an application's deployed dependencies can be pinned?

Section 31

A release checklist turns the steps of publishing a new version into a repeatable routine, so that nothing important is forgotten under time pressure. A typical checklist includes updating the version number, updating the changelog, running the full test suite, building distributions, installing those distributions in a clean environment, running import and smoke tests, checking the distributions' contents, and then uploading or sharing them. Each step has a purpose, and skipping one, such as clean-environment testing, can allow a packaging mistake to reach users, even if the code itself is correct. Following the checklist in order, and marking each step complete, makes releases calmer and more reliable.

The checklist should be written down, not kept only in memory, because memory is unreliable, especially for steps that are performed infrequently. A short checklist in the project's repository, perhaps in a contributor guide or a release document, serves as a reference for the author and for any future maintainer. It also serves as a training tool for a new contributor who is preparing their first release, by showing them the steps that experienced maintainers follow. A checklist that is written down is a checklist that can be improved over time, as lessons are learned from past releases.

Some steps on the checklist can be automated, as discussed in the context of continuous integration, while others require human judgement, such as reviewing the changelog for clarity or deciding whether a change warrants a major version. Automating the mechanical steps, such as building and testing, reduces the chance of human error, while leaving judgement steps to a person ensures that releases are thoughtful, not merely automatic. That combination, of automation for mechanics and judgement for decisions, is a good model for release processes generally.

It is also useful to include a post-release step on the checklist: verifying that the published package can be installed from its intended source, in a clean environment, and that its version and contents match what was intended. That verification catches mistakes such as uploading the wrong file, or publishing to the wrong index, which are embarrassing and potentially confusing for users, but which are easy to catch with a quick install test after publication. A post-release check, performed immediately after uploading, turns a potential problem into a quick correction, before many users have installed the faulty version.

Finally, if a release goes wrong, the checklist should include a recovery step: how to withdraw or replace a faulty version, how to communicate the problem to users, and how to prepare a corrected version. Planning for failure, while hoping it does not happen, is part of responsible release management, because mistakes do happen, even with checklists. A project with a recovery plan can respond calmly and clearly when a release fails, while one without a plan may react hastily, making the problem worse. That preparedness is a mark of a mature maintainer.

Check: Why should a release checklist be written down rather than kept only in memory?

Section 32

Handling bug reports for packaged code requires a process that connects a user's report to a fix and a new release. When a user reports that a package does not work as expected, the first information needed is the version they installed, the environment they used, and the steps that reproduce the problem. That information, which a good issue template can prompt for, lets the maintainer reproduce the problem in a clean environment, rather than guessing from a vague description. Reproduction is the foundation of fixing, as discussed in earlier chapters, and it applies to packaged code just as it applies to code in development.

Once a bug is reproduced, the fix should be made in the source code, tested with the project's test suite, and, ideally, accompanied by a new test that would have caught the bug, so that it does not return in a later version. That test, added alongside the fix, turns a one-off correction into a lasting improvement in the project's quality. The fix should then be released as a new version, following the release checklist, with the changelog noting the fix and crediting the reporter if appropriate. That cycle, from report to fix to release, is how trust is built between a maintainer and users.

It is important to distinguish between bugs in code and bugs in packaging. A user may report that a package fails to import, or that a command is missing, when the code itself is correct but the distribution is incomplete. Diagnosing that distinction requires testing the installed package, not just the source code, as discussed earlier. If the problem is packaging, the fix lies in metadata, in included files, or in dependencies, rather than in code, and the release that fixes it should be a packaging fix, with a changelog entry that says so, so that users understand what changed.

Communication during bug handling matters as much as the fix itself. Acknowledging a report promptly, even if only to say that it is being investigated, reassures the reporter that their effort was valued. Updating the report as the investigation progresses, and noting when a fix is released, closes the loop and encourages future reports. A maintainer who communicates clearly, even when a fix will take time, maintains a better relationship with users than one who fixes silently without acknowledgement, because users feel heard and informed.

Finally, not every bug report requires an immediate fix. Some reports describe behaviour that is actually intended, or that results from a misunderstanding of documentation, in which case clarifying documentation may be the right response. Others describe a rare edge case that is less important than other work, in which case acknowledging the report and scheduling the fix for a later version is honest and reasonable. Prioritising bug reports, based on severity, frequency, and impact, is part of maintenance, and it should be done transparently, so that reporters understand why their issue is or is not being fixed immediately.

Check: How can you distinguish a packaging bug from a code bug when a user reports a failure?

Section 33

Security updates for dependencies are a maintenance responsibility that should be taken seriously, because dependencies can contain vulnerabilities that are discovered after a project is released. When a vulnerability is reported in a dependency, the project author should assess whether their project is affected, update the dependency to a fixed version if needed, test that the update does not break the project, and release a new version with the fix, noting it in the changelog. That process, while it may seem burdensome for a small project, protects users who depend on the project, often without knowing which dependencies it uses.

Staying informed about dependency vulnerabilities requires a source of information, such as security advisories from a package index, from the dependency's own project, or from a security scanning tool. For a small project, checking advisories occasionally, or enabling notifications from a trusted source, may be sufficient. For a larger project, automated scanning, which checks dependencies against known vulnerability databases on every change or on a schedule, provides more consistent coverage. Whichever approach is used, it should be deliberate, not accidental, because vulnerabilities that are never checked for will never be fixed.

When updating a dependency for security, it is important to test the update, even if it is a small version change, because a security fix can occasionally change behaviour in a way that affects the project. Running the project's test suite, and smoke testing its main features, after a dependency update, confirms that the fix does not introduce a regression. If the update does break the project, the author must decide whether to adapt the project to the new version, or to find an alternative, and that decision should be documented, so that users understand why a particular version is required.

It is also important to avoid introducing vulnerabilities through packaging itself. Including unnecessary files, such as credentials or private data, in a distribution, as discussed earlier, is a packaging security mistake that can have serious consequences. Similarly, depending on a package from an untrusted source, or using a name that mimics a popular package, can expose users to risk. Following standard packaging practices, using trusted indexes, and inspecting distributions before sharing them, are basic security habits that protect both authors and users.

Finally, security maintenance should be communicated honestly. If a project has a known vulnerability that has not yet been fixed, saying so, with guidance on mitigation or on a workaround, is better than silence, because users can then make informed decisions about whether to continue using the project. If a vulnerability has been fixed, noting it in the changelog, without necessarily detailing how it could be exploited, lets users know they should update. That honest communication, balanced with care not to provide instructions for exploiting a vulnerability, is part of responsible security practice for packaged software.

Check: What steps should an author take when a vulnerability is reported in a dependency?

Section 34

Deprecating a feature means marking it as outdated and planned for removal, while still allowing it to work for a transition period, so that users have time to update their code. Deprecation is a kinder alternative to abrupt removal, because it gives users notice, guidance on what to use instead, and time to migrate, rather than breaking their code without warning. It is an important tool for maintaining a package over time, because projects evolve, and features that were once useful may become redundant, confusing, or difficult to maintain, and removing them eventually is necessary for the project's health.

A deprecation should be communicated in several places: in documentation, where the feature is described as deprecated and its replacement is named; in the changelog, where the deprecation is announced and a timeline for removal is given, if known; and, where appropriate, in code, through a warning that is shown when the deprecated feature is used. That warning should name the feature, state that it is deprecated, and say what to use instead, so that users who encounter it can act immediately, without needing to search documentation. Warnings should be clear but not alarming, because a deprecated feature still works, and users should not panic, but they should plan to migrate.

The timeline for removal should be reasonable. Removing a deprecated feature in the very next version, without giving users time to update, defeats the purpose of deprecation. A common practice is to deprecate in one minor version and remove in the next major version, giving users at least one full minor release cycle to migrate. For widely used features, a longer timeline may be appropriate. Whatever timeline is chosen, it should be stated in the deprecation notice, so that users can plan, rather than guessing when removal will happen.

During the deprecation period, the deprecated feature should still work correctly, and it should still be tested, because users are still relying on it. Neglecting a deprecated feature, so that it breaks before it is removed, is worse than removing it promptly, because it creates a confusing situation where a feature is documented as working but actually fails. Maintaining deprecated features until their planned removal, with tests to ensure they still work, is part of the commitment that deprecation implies.

Finally, when a deprecated feature is finally removed, that removal should be noted prominently in the changelog, and, if the project follows semantic versioning, it should occur in a major version, because it is a breaking change for users who have not yet migrated. That major version bump signals to users that they should check the changelog before updating, and it gives them a clear point at which to migrate if they have not already done so. Deprecation, done well, turns a potentially disruptive change into a managed transition, which is a hallmark of a mature, user-respecting project.

Check: Why is deprecation kinder to users than abrupt removal of a feature?

Section 35

Supporting users of a packaged project involves more than fixing bugs; it involves providing channels for questions, guidance, and feedback, and setting expectations about what support is available. For a small project, support might simply be an email address or an issue tracker where users can ask questions, with a note in the readme about expected response times. For a larger project, support might include documentation, examples, a discussion forum, or a chat channel. Whatever the scale, making support channels visible and setting expectations honestly helps users know where to go for help, and helps authors manage the time they spend on support.

Issue templates are a useful tool for support, because they prompt users to provide the information needed to help them, such as the version used, the steps taken, and the error encountered. A template for bug reports, a template for feature requests, and a template for questions, each with appropriate prompts, can improve the quality of reports and reduce the back-and-forth needed to gather basic information. Templates should be short and friendly, not bureaucratic, because a template that feels like a form to be filled in may discourage users from reporting at all. The goal is to guide, not to gatekeep.

Documentation is also a form of support, because good documentation answers many questions before they are asked. A readme with examples, a guide covering common tasks, and a reference for the project's interface, together, form a self-service support system that users can consult at any time, without waiting for a maintainer. Investing in documentation therefore reduces support burden over time, while also improving the project's usability. That investment pays off especially for projects with many users, where answering the same question repeatedly would consume significant maintainer time.

Setting expectations about support is important, especially for projects maintained by volunteers or as learning exercises. A readme note stating that the project is maintained in spare time, that responses may take a few days, or that the project is provided as-is without guaranteed support, helps users calibrate their expectations and decide whether to depend on the project. Honest expectations prevent frustration, both for users who might otherwise expect immediate help, and for maintainers who might otherwise feel overwhelmed by demands they never agreed to meet.

Finally, support should be kind and welcoming, even when a user's question reveals a misunderstanding or a mistake. A helpful response, pointing to documentation or explaining a concept, encourages users to continue using the project and to contribute back, perhaps by improving documentation or reporting problems more clearly in future. A dismissive response, by contrast, discourages engagement and harms the project's community, however small. Kindness in support is not just politeness; it is a practical investment in a project's future, because users who feel welcome are more likely to become contributors.

Check: How do issue templates improve the quality of bug reports without discouraging users?

Section 36

Packaging for different audiences requires thinking about who will install a project and what they need from it. A project for beginners, such as a learning tool or a simple utility, should be as easy to install as possible, with minimal steps, clear instructions, and a single, obvious command. Its documentation should assume little prior knowledge, and its error messages should be friendly and explanatory. A project for experienced developers, by contrast, can assume more knowledge, can offer more configuration options, and can focus its documentation on reference material rather than tutorials, because its users will already understand the concepts involved.

For beginners, the installation experience is critical, because a beginner who cannot install a project will not use it, no matter how good its code is. That means testing installation in a clean environment, as a beginner would experience it, and ensuring that the instructions work exactly as written, without requiring unstated prerequisites. It also means avoiding jargon in installation instructions, and explaining any terms that are necessary, such as virtual environments, in plain language. A beginner-friendly package is one where the path from discovery to first successful use is short and smooth.

For experienced developers, the priorities shift. They value precise metadata, flexible dependency ranges, clear versioning, and a well-structured codebase that they can read and contribute to. They may install a project into an existing environment alongside other projects, so dependency flexibility and conflict avoidance matter more than a single, simple install command. They may also want to install from source, to inspect or modify the code, so a complete and correct source distribution matters. Meeting those needs requires attention to packaging details that beginners may not notice, but that experienced users will appreciate.

A project may serve both audiences, in which case its packaging and documentation should provide a simple path for beginners and a detailed path for experienced users, without confusing either group. A readme that starts with a simple installation and first example, and then links to advanced documentation for developers, serves both. Metadata that is complete and accurate serves both, because it helps installers and tools regardless of the user's experience. That layered approach, simple on the surface and detailed underneath, is a good model for projects with a broad audience.

Finally, audience awareness should inform decisions about support and communication, as discussed earlier. A beginner audience may need more guidance and patience, while an experienced audience may expect faster, more technical responses. Tailoring support style to the audience, while remaining kind and clear in both cases, helps a project serve its users well, whatever their experience level. Packaging, documentation, and support together form the project's user experience, and that experience should be designed for its actual audience, not for an imagined generic user.

Check: How should installation instructions differ for a beginner audience versus an experienced audience?

Section 37

A repository containing multiple packages, sometimes called a monorepo, is a way to organise several related projects together, with shared tooling, shared documentation, and a single place for contributors to work. Instead of each project living in its own repository, with its own configuration and history, a monorepo holds them all, allowing changes that affect several projects to be made and tested together. That organisation can be efficient for a team working on a suite of related tools, because it reduces duplication of setup and makes cross-project changes easier to coordinate.

Monorepos also bring complexity. Each package in a monorepo still needs its own metadata, its own version, and its own distribution files, because users install packages individually, not the whole repository. Build tools must therefore be configured to build each package separately, from its own folder within the repository, and to handle dependencies between packages, where one package in the repository depends on another. That configuration is more complex than for a single-package repository, and it requires careful testing to ensure that each package builds and installs correctly on its own, not just as part of the repository.

Versioning in a monorepo deserves thought. One approach is to version all packages together, releasing them all with the same version number, even if only one changed. That approach is simple, but it can confuse users, who may wonder why a package's version increased when its code did not change. Another approach is to version each package independently, releasing only those that changed, with their own version numbers. That approach is more precise, but it requires tooling to track which packages changed and to release only those. Choosing between those approaches depends on how closely related the packages are, and on how the team works, and the choice should be documented so that users understand the versioning they see.

For a small project, or for a learner's first packages, a monorepo is usually unnecessary complexity. A single repository per project is simpler to understand, to package, and to maintain, and it avoids the configuration overhead of multi-package builds. A monorepo becomes worthwhile when the number of related projects grows to the point where separate repositories create more overhead than they save, such as duplicated CI configuration, duplicated documentation setup, and difficulty coordinating changes across projects. That threshold varies by team, but it is generally higher than one or two projects.

Finally, whether a project lives in a monorepo or in its own repository, the packaging principles from this chapter still apply: clear metadata, tested distributions, clean-environment installation, and good documentation for each package. A monorepo does not change those principles; it only changes how they are organised and automated. Keeping those principles in focus, while choosing an organisational structure that fits the project's scale, leads to packaging that is both manageable and reliable, whatever the repository layout.

Check: Why does each package in a monorepo still need its own metadata and version?

Section 38

Version control and packaging are closely linked, because tags in version control often mark releases, and those tags should correspond to published versions. A tag is a named marker pointing to a particular commit, usually named after the version it represents, such as v1.2.3. When a new version is released, creating a tag for that version preserves a record of exactly which code was released, allowing anyone to check out that code later, to reproduce a bug, or to compare versions. That record is valuable for maintenance, for auditing, and for trust, because it ties a published distribution to a specific point in the project's history.

The process of tagging should be part of the release checklist, performed at the same time as updating the version number and changelog, so that the tag, the version, and the changelog all agree. If a tag is created for the wrong commit, or if the version number in metadata does not match the tag name, confusion follows, because users and tools cannot tell which code a version refers to. Checking that agreement, as part of release verification, prevents that confusion, and it is a small step that pays off when a past version needs to be investigated.

Tags also enable automated release processes, where a CI service detects a new tag and builds, tests, and publishes the corresponding version automatically. That automation, discussed in the context of CI, reduces manual steps and the chance of human error, but it also requires care, because an accidental tag could trigger an unintended release. Protecting tags, by requiring review before a tag is created, or by using a separate, deliberate step to trigger a release, prevents accidental publications. Automation should make releases easier, not riskier, and that requires thoughtful configuration.

Branching strategy interacts with packaging too. A project may maintain a main branch for development, and release branches for stable versions that receive only fixes, not new features. That strategy allows a stable version to be maintained, with patch releases for serious bugs, while development continues on the main branch towards the next major version. Packaging for release branches involves building from the branch, with its own version numbers and changelogs, and ensuring that fixes applied to a release branch are also applied to the main branch where appropriate, so that they are not lost in the next release.

Finally, version control history itself is a form of documentation for packaging decisions. Commit messages that explain why a dependency was added, why a version was bumped, or why a packaging configuration changed, help future maintainers understand the project's evolution. Writing clear commit messages for packaging changes, just as for code changes, is therefore part of good packaging practice, and it turns version history into a useful record rather than a list of vague entries. That record, combined with tags and changelogs, forms a complete history of a project's releases.

Check: Why should a release tag, version number, and changelog all agree with each other?

Section 39

Automating releases, beyond CI testing, means using scripts or services to perform release steps, such as building distributions, uploading them, and creating tags, with minimal manual intervention. Automation can reduce errors, such as forgetting a step or uploading the wrong file, and it can make releases faster and more consistent, which encourages more frequent, smaller releases, which are generally safer than large, infrequent ones. However, automation also requires trust in the automation itself, and it should be introduced gradually, starting with the most mechanical steps, while keeping human judgement for decisions such as whether a version is ready to release.

A simple form of release automation is a script that performs the release checklist steps in order: updating the version, building distributions, testing them, and prompting for confirmation before uploading. That script, kept in the project's repository, documents the release process in executable form, so that it can be followed consistently and improved over time. It also serves as a training tool for new maintainers, who can read the script to understand what a release involves, rather than relying on tribal knowledge. A scripted release process is more reliable than a manual one, even if it still requires human confirmation at key steps.

More complete automation, where a tag triggers a CI service to build, test, and publish automatically, removes manual steps entirely, but it also removes the opportunity for a human to catch a mistake before publication. That trade-off should be weighed carefully, especially for public projects, where a faulty publication can affect many users. A middle path, where automation builds and tests, but a human must approve publication, combines the consistency of automation with the judgement of a human, and it is often the best balance for projects that are not releasing very frequently.

Automation should also handle failure gracefully. If a build fails, or a test fails, during an automated release, the automation should stop, report the failure clearly, and not publish a partial or faulty release. That fail-safe behaviour is essential, because an automated system that publishes despite failures is worse than a manual process that catches them. Testing automation itself, by performing a trial release to a test index, as discussed earlier, confirms that it behaves correctly, including in failure cases, before it is trusted with real releases.

Finally, automated releases should still produce human-readable artefacts: a changelog entry, a tag, and a release note, so that users can understand what changed, even if the release was produced automatically. Automation that produces releases without documentation leaves users guessing, which undermines trust, even if the releases themselves are correct. Keeping documentation as part of automated releases, by generating it from the changelog or by requiring a changelog update before automation runs, ensures that automated releases remain understandable to humans, which is ultimately what matters.

Check: Why is a human approval step valuable even in an otherwise automated release process?

Section 40

Packaging, as covered in this chapter, is about making code installable, usable, and maintainable by others, and its principles can be summarised in a few habits. The first habit is clarity: declare metadata completely and accurately, name projects and packages clearly, and document installation and use in a readme that a new user can follow. Clarity in packaging, as in code, reduces confusion and increases trust, because users can see what a project is, what it needs, and how to use it, without guessing.

The second habit is testing in a clean environment. Building distributions, installing them where no development files or undeclared dependencies are present, and running imports and smoke tests, catches the packaging mistakes that development-environment testing misses. That habit, applied to every release, is the single most effective packaging quality practice, and it should become a reflex, not an occasional check. A package that has been tested cleanly is a package that can be shared with confidence.

The third habit is versioning and changelog discipline. Increasing versions consistently, never reusing a version, documenting changes from the user's perspective, and deprecating features gracefully before removing them, together, make updates predictable and safe for users. Those habits respect users' investment in a project, by giving them information and time to adapt to changes, rather than surprising them with breaking updates. Versioning discipline is a form of respect, expressed through process.

The fourth habit is maintenance. Updating dependencies, responding to bug reports, releasing fixes promptly, and retiring projects gracefully when they are no longer needed, together, keep a package alive and trustworthy over time. Maintenance is less exciting than initial development, but it is what distinguishes a project that users can depend on from one that they try once and abandon. Committing to maintenance, at a level appropriate to the project's audience, is part of the decision to share a package.

As you move on, the best way to consolidate this chapter is to package a real project of your own, however small, following the checklist from earlier in the chapter. Build its distributions, test them in a clean environment, write its readme and changelog, and, if appropriate, share it privately with a colleague or publicly on a test index. That single, complete packaging exercise will teach you more than reading about packaging, because it will confront you with the details that only real distribution reveals. Keep the habits from this chapter, and your future projects will be easier to share, easier to maintain, and more useful to others.

A packaged project that follows these habits also becomes easier for others to contribute to, because its structure, metadata, and release process are clear enough for a newcomer to understand and follow.

Check: What four habits summarise this chapter's approach to packaging?

Section 41

Packaging and documentation sites work together, because a package index shows a project's summary, while a documentation site shows its full guide, reference, and examples. For a small project, the readme may be sufficient documentation, and a separate site may be unnecessary. For a larger project, with many features, a documentation site, organised into sections, with search and navigation, is far easier to use than a single long readme. Deciding whether a project needs a separate documentation site, or whether its readme suffices, depends on its size, its complexity, and its audience, and that decision can be revisited as the project grows.

When a documentation site is needed, it should be built from source files kept in the project's repository, so that documentation changes are versioned alongside code changes, and so that contributors can update documentation with the same workflow as code. That source-based approach also allows documentation to be built and published automatically, for example on every release, ensuring that the published documentation matches the released code. Keeping documentation source in the repository, rather than editing a website directly, preserves history and enables review, just as for code.

Links between packaging and documentation should be consistent. The package metadata should point to the documentation site, the readme should link to it, and the documentation site should link back to the package's installation instructions and source repository. That web of links helps users navigate from discovery, on a package index, to installation, via the readme, to detailed use, via documentation, without getting lost. Broken links in that web, as noted earlier, undermine trust, so they should be checked as part of release verification, ideally automatically, but at least manually for major releases.

Versioned documentation, where each major version has its own documentation set, is valuable for larger projects, because users on an older version need documentation for that version, not only for the latest. For small projects, a single documentation set, with notes about version differences where they matter, is usually sufficient, and it is far better than no documentation. The key is to ensure that documentation does not mislead users about the version they have installed, by clearly stating which version it describes, especially when features have changed between versions.

Finally, documentation sites, like packages themselves, need maintenance. As code changes, documentation must be updated, examples must be retested, and links must be checked. A documentation site that is out of date is worse than no site, because it actively misleads users who trust it. Treating documentation maintenance as part of release maintenance, with documentation updates included in the release checklist, keeps the site accurate and trustworthy, and it ensures that packaging and documentation evolve together, rather than drifting apart.

Check: Why should documentation source live in the project's repository rather than only on a website?

Section 42

Platform-specific packaging arises when a project needs different behaviour, dependencies, or files on different operating systems. A project might need a particular system tool on one platform, a different dependency on another, or a platform-specific configuration file. Handling those differences in packaging requires declaring them in metadata, where supported, so that installers can choose the right dependencies for the user's platform, rather than installing everything for every platform. That platform-aware declaration keeps installations lean and avoids installing packages that cannot work on a given system.

Conditional dependencies, which are dependencies that apply only on a particular platform or Python version, are a standard way to express platform differences. For example, a project might depend on a package only on Windows, because that package provides functionality that is built into other systems. Declaring that dependency conditionally, rather than unconditionally, means that users on other systems do not install an unnecessary package, and users on Windows get the package they need automatically. That precision is better than either installing everything everywhere, which bloats installations, or documenting a manual installation step, which users may forget.

Platform-specific code, as opposed to dependencies, should be handled in code, with checks for the operating system, rather than by distributing different code for different platforms, where possible. A single codebase that adapts to its platform, by choosing the right system calls or file paths at run time, is easier to maintain than several platform-specific codebases, because fixes and features need to be made only once. That single-codebase approach, combined with platform-aware metadata for dependencies, is the standard way to handle platform differences in pure Python projects, which are largely portable by design.

Testing across platforms, as discussed in the context of CI, is essential for platform-specific packaging. A dependency declared conditionally for one platform should be tested on that platform, to confirm that it installs and works, and a project that claims to support several platforms should be tested on each, at least for its main features. Testing only on the developer's platform, while claiming support for others, risks publishing a package that fails for users on other systems, which damages trust and generates bug reports that could have been prevented by broader testing.

Finally, platform differences should be documented. A readme note stating which platforms are supported, and any platform-specific installation steps or limitations, helps users judge whether a project will work for them before they install it. That documentation should be honest about limitations, such as a feature that works only on one platform, rather than implying universal support. Honest platform documentation, combined with conditional dependencies and cross-platform testing, makes a package genuinely portable, rather than merely claiming to be.

Documenting which platforms have been tested, and which have not, also helps users decide whether to try a project on an untested platform or to wait for broader support.

Check: Why are conditional dependencies better than installing every platform's packages everywhere?

Section 43

Testing packaging across environments, using tools that automate creation of clean environments, is a step beyond manual clean-environment testing, and it is worth understanding even if you do not adopt it immediately. Such tools create a fresh environment, install the project and its dependencies, run its tests, and then discard the environment, repeating the process for different configurations, such as different Python versions or different dependency sets. That automation turns a manual, error-prone process into a repeatable one, and it catches packaging and compatibility problems that manual testing might miss, because it is performed consistently on every change.

The value of automated environment testing lies in its isolation. Each test run starts from a known, clean state, without leftover files, installed packages, or configuration from previous runs, so a passing test means that the project truly works from scratch, not that it works because of something left over from an earlier run. That isolation is hard to achieve manually, because developers' environments accumulate packages and files over time, masking missing dependencies or files. Automated clean-environment testing restores that isolation for every run, making test results trustworthy.

Configuring automated environment testing involves declaring which environments to test, such as which Python versions and which dependency sets, and which commands to run in each environment. That configuration lives in the project's repository, alongside code, so that it is versioned and reviewed like other changes. Starting with a simple configuration, testing on one or two environments, and expanding it as the project grows, is a practical approach, because it avoids the complexity of a large matrix before it is needed, while still providing the core benefit of clean-environment testing.

There is a cost to automated environment testing, in time and in complexity. Each environment takes time to create and test, so a large matrix can make test runs slow, which may discourage developers from running tests frequently. Balancing coverage against speed, by testing the most important combinations on every change and the full matrix less frequently, such as before a release, is a sensible compromise. That balance, like many packaging decisions, depends on the project's size, its audience, and its release frequency, and it should be revisited as the project evolves.

Finally, automated environment testing complements, but does not replace, the manual release checks discussed earlier. An automated test can confirm that a package installs and its tests pass, but it cannot confirm that its readme is clear, that its changelog is accurate, or that its version number is correct. Those human checks remain part of a release, even for a project with excellent automation. Automation handles the mechanical, repeatable checks reliably, freeing humans to focus on judgement, documentation, and design, which is where human attention adds the most value.

Check: Why does automated clean-environment testing give more trustworthy results than testing in a developer's own environment?

Section 44

Discoverability is the question of how users find a package among the many available, and it starts with naming and description. A package with a clear, descriptive name, and a readme that explains its purpose in plain language, is more likely to be found by a user searching for a solution to a problem, because search tools match names and descriptions against search terms. A package with a cryptic name, or a readme that assumes the reader already knows what the project does, is less likely to be found, even if its code is excellent. Discoverability is therefore a packaging concern, not just a marketing concern, because it determines whether a useful project reaches its audience.

Keywords and categories in metadata can also help discoverability, where a package index supports them, by allowing users to browse or filter by topic. Choosing keywords that describe the project's domain, its purpose, and its audience, rather than generic terms that could apply to any project, helps the right users find it. For example, a package for summarising CSV files might use keywords related to CSV, data, and reporting, rather than a generic keyword such as tool. That specificity helps search and browsing, and it is a small effort that pays off in visibility.

Documentation and examples also affect discoverability, because users often find projects through tutorials, blog posts, or examples that mention them, rather than through direct search of a package index. A project with clear examples, in its readme and in its documentation, is more likely to be mentioned, linked to, and recommended, because it is easy to demonstrate its value quickly. A project that is hard to demonstrate, because its first example is complex or requires extensive setup, is less likely to spread by word of mouth, even if it is powerful once set up.

Reputation and maintenance also influence discoverability over time. A project that is well maintained, with regular releases, responsive issue handling, and clear documentation, builds a reputation that leads to recommendations and links, which in turn increase its visibility. A project that appears abandoned, with no recent releases and unanswered issues, may be passed over in favour of a maintained alternative, even if its code is similar. Maintenance, discussed earlier, is therefore not just about correctness; it is also about visibility, because users prefer projects they trust to remain available and supported.

Finally, discoverability should not be pursued through misleading practices, such as using a name similar to a popular project to attract mistaken installs, or stuffing a description with irrelevant keywords. Those practices, besides being dishonest, can lead to a project being removed from an index or distrusted by users who discover the deception. Honest discoverability, through a clear name, an accurate description, good documentation, and reliable maintenance, builds lasting visibility, while misleading practices build only short-term attention at the cost of long-term trust.

Check: How do a clear name and plain-language description help users discover a package?

Section 45

Open source packaging etiquette is the set of unwritten expectations that govern how projects are shared, credited, and maintained in a community. One expectation is attribution: when you use another project's code, you should credit its authors, respect its licence, and not present their work as your own. That attribution is usually expressed through licence files, author fields in metadata, and acknowledgements in documentation, and it is a basic courtesy that also has legal significance, because licences often require attribution as a condition of use. Respecting attribution builds trust in a community where code is freely shared.

Another expectation is responsiveness, tempered by realism. Maintainers are often volunteers, and users should understand that responses may take time, and that not every feature request will be accepted. Conversely, maintainers should acknowledge contributions, even if only to say thank you and explain why a contribution cannot be accepted, because contributors who feel ignored are less likely to contribute again. That mutual respect, between users who report problems thoughtfully and maintainers who respond thoughtfully, is what sustains open source communities, and packaging practices, such as clear issue templates and changelogs, support it by making communication easier.

A third expectation is not to break others unnecessarily. When a project is depended upon by others, changing its interface, its behaviour, or its dependencies without notice or without a major version bump, as discussed earlier, breaks downstream projects and frustrates their maintainers. Following versioning discipline, deprecating before removing, and communicating changes clearly, are etiquette as much as they are technical practices, because they show respect for the time and work of downstream users. A maintainer who follows those practices earns a reputation for reliability, which encourages adoption.

Contributing to packaging itself is also part of etiquette. If you find a packaging bug in another project, such as a missing dependency, reporting it clearly, with steps to reproduce, helps the maintainer fix it, and offering a fix, where appropriate, helps even more. That contribution, while less glamorous than adding a feature, is valuable, because packaging bugs affect every user, and fixing them improves the project for everyone. Participating in packaging, through reports and fixes, is a good way for a newcomer to contribute to a project they use, without needing deep knowledge of its code.

Finally, open source etiquette includes giving back when you benefit. If a project saves you significant time, considering how you might contribute, whether through code, documentation, testing, or simply by reporting problems clearly, is a way to sustain the commons that you benefit from. That contribution need not be large; even a clear bug report, or a documentation fix, helps. A community where users contribute back, in small ways, is more sustainable than one where a few maintainers do all the work while many users only consume, and packaging, as the interface between a project and its users, is a natural place for that contribution to start.

Check: Why is following versioning discipline considered etiquette as well as a technical practice?

Section 46

A teaching project, such as a small tool built for a course or a tutorial, has packaging needs that differ slightly from a production project, but the same principles apply. A teaching project should be easy to install, so that learners can focus on the concepts being taught, rather than on installation problems. That means testing installation in a clean environment, as a learner would experience it, and providing instructions that assume little prior knowledge, with explanations for any steps that might be unfamiliar, such as creating a virtual environment. A teaching project that is hard to install teaches frustration, not programming.

A teaching project's code should also be readable as an example. Learners will read its source, its metadata, and its packaging configuration, as models for their own projects, so those files should be clear, well commented where needed, and free of unnecessary complexity. A teaching project that uses an elaborate packaging setup, with many advanced features, may confuse learners who are still grasping the basics, while one that uses a simple, standard setup, with clear explanations for each part, teaches packaging effectively by example. Simplicity, in a teaching project, is a pedagogical choice, not just a practical one.

Documentation for a teaching project should explain not only how to use the project, but also how it was packaged, and why particular choices were made. A readme that includes a short section on packaging, explaining the layout, the metadata, and the build process, turns the project itself into a teaching resource for packaging, in addition to its main topic. That dual purpose, as both a tool and an example, is valuable in a learning context, because learners can see packaging in a real, working project, rather than only reading about it in abstract.

Versioning and releases for a teaching project can be simpler than for a production project, because its audience is smaller and its changes are often tied to course updates rather than to user needs. However, basic discipline, such as increasing versions for each release and noting changes, still applies, because it models good practice for learners, who will carry those habits into their own projects. A teaching project that models sloppy versioning teaches sloppy habits, even if its code is correct, so maintaining basic discipline is part of its educational role.

Finally, a teaching project should be honest about its limitations. If it is a simplified example, not intended for production use, saying so prevents learners from mistaking it for a robust tool, and it helps them understand what would need to change for production use. That honesty, combined with clear code, simple packaging, and good documentation, makes a teaching project a trustworthy learning resource, and it demonstrates the packaging principles from this chapter in a context that learners can understand and emulate.

Check: Why should a teaching project's packaging be simple and clearly explained, rather than elaborate?

Section 47

Troubleshooting installation failures is a skill that combines packaging knowledge with systematic debugging. When a user reports that a package fails to install, the first step is to gather information: the exact command run, the full error message, the Python version, and the environment, such as whether a virtual environment was used. That information, which a good issue template prompts for, often reveals the problem immediately, such as a typo in the package name, an unsupported Python version, or a missing system tool. Without that information, troubleshooting becomes guessing, which wastes time for both the reporter and the maintainer.

Common installation failures have common causes. A failure mentioning a missing module during installation often indicates a missing build-time dependency, or a build backend that is not available. A failure mentioning a version conflict often indicates that two packages require incompatible versions of a dependency, which may be resolved by updating one package, or by using separate environments. A failure mentioning a file not found often indicates a packaging mistake, such as a missing data file or an incorrect path. Recognising those patterns, from error messages, speeds up diagnosis, and it is a skill that improves with experience of packaging failures.

Reproducing an installation failure in a clean environment is the next step, after gathering information. If the failure reproduces cleanly, it is likely a real packaging problem, and the fix lies in metadata, dependencies, or included files. If it does not reproduce, the problem may be specific to the reporter's environment, such as a corrupted cache, a misconfigured environment, or a conflicting package installed for another project. That distinction, between a reproducible packaging bug and an environment-specific problem, guides the fix: a packaging fix for the former, and environment guidance, such as recreating a virtual environment, for the latter.

Clearing caches and recreating environments are common troubleshooting steps for environment-specific problems, but they should be suggested carefully, because they can delete a user's work if done carelessly. Advising a user to create a new, clean virtual environment and test installation there, rather than deleting their existing environment outright, is safer, because it preserves their work while testing a hypothesis. That cautious approach to troubleshooting, like cautious file handling in earlier chapters, prevents a troubleshooting step from becoming a new problem.

Finally, troubleshooting should be documented, both for the user and for future maintainers. If a particular installation failure recurs, adding a note to documentation, or fixing the underlying packaging issue so that it does not recur, prevents future users from encountering the same problem. A troubleshooting guide, built from real reports, becomes a valuable part of a project's documentation, and it shows that the maintainer takes installation problems seriously, which builds trust in the project as a whole.

Check: Why is reproducing an installation failure in a clean environment an important troubleshooting step?

Section 48

Packaging standards and tools evolve over time, and a maintainer should expect that today's recommended practices may change in the future. New standards may replace old configuration formats, new tools may offer better ways to build and install, and old practices may become discouraged, even if they still work. That evolution is a sign of a healthy ecosystem, where lessons learned lead to improvements, but it also means that packaging knowledge needs periodic updating, and that projects need occasional maintenance to stay current with standards. Staying informed, through packaging guides, changelogs of tools, and community discussions, is part of maintaining a package over time.

When standards change, updating a project should be done deliberately, not hastily. A new standard may offer benefits, such as simpler configuration or better reproducibility, but migrating to it also carries risk, such as introducing a packaging mistake during the transition. Testing the migration in a clean environment, as with any packaging change, and documenting why the migration was made, ensures that the change is safe and understandable. Migrating because a standard has changed, without understanding the benefit, can add churn without value, while migrating thoughtfully, when the benefit is clear, keeps a project current and maintainable.

Backward compatibility with older tools is also a consideration when standards evolve. A project that adopts a very new standard may fail to install for users with older tools that do not yet support it, even if its code would work. Balancing adoption of new standards against support for users with older tooling requires judgement, based on the project's audience and on how widely the new standard has been adopted. For a project with a broad audience, waiting until a standard is widely supported before adopting it may be wiser than adopting it immediately, while for a project with a technical audience that updates tools frequently, early adoption may be appropriate.

It is also worth remembering that packaging fundamentals change more slowly than specific tools. The ideas in this chapter, such as clear metadata, clean-environment testing, versioning discipline, and good documentation, have remained relevant through several changes in packaging tools, and they are likely to remain relevant through future changes. Focusing on fundamentals, while staying informed about tools, ensures that a maintainer's skills remain valuable even as specific configuration formats evolve. Tools are a means to those fundamentals, not an end in themselves.

Finally, contributing to packaging standards and tools, by reporting problems, suggesting improvements, or helping to test new features, is a way to shape the ecosystem's evolution, rather than merely reacting to it. That contribution, like other open source contributions, requires time and expertise, but it benefits the whole community, by making packaging better for everyone. Even a small contribution, such as a clear bug report about a packaging tool, helps, and it is a natural extension of the packaging skills developed in this chapter.

Check: Why should migration to a new packaging standard be done deliberately and tested, rather than hastily?

Section 49

A practical packaging exercise, bringing together the steps from this chapter, is the best way to test your understanding. Choose a small project you have already written, such as a command-line tool from the previous chapter, and package it following a checklist. Start by organising its layout, with code in a package folder, a readme, a licence, and a metadata file. Declare its name, version, description, and dependencies, and define an entry point for its command, if it is a command-line tool. That initial organisation, done carefully, is the foundation for everything that follows.

Next, build its distributions, inspect their contents, and test them in a clean virtual environment. Install the built package, run its command or import its modules, and confirm that it works as expected. Test its help output, its error handling, and its main features, with sample data, in the clean environment, not in your development folder. If any test fails, fix the packaging or the code, rebuild, and retest, until the clean-environment test passes. That cycle, of build, test, fix, and rebuild, is the core of packaging quality, and experiencing it with a real project teaches more than reading about it.

Then, write or update the project's readme and changelog, to reflect its packaged form. The readme should explain installation, with a single command for the packaged form, and show a first example that succeeds. The changelog should note the first packaged release, with its version, and describe what the project does, in user-facing language. Those documents, written for a new user, complete the package's public face, and they should be checked for accuracy by following their instructions exactly, as a new user would.

Finally, share the package in a low-stakes way, such as by sending its wheel file to a colleague, or by uploading it to a test index, and ask the recipient to install and use it, reporting any problems. That external test, by someone other than the author, often reveals assumptions that the author did not notice, such as an undocumented prerequisite or a confusing instruction. Acting on that feedback, and updating packaging or documentation accordingly, closes the loop from author to user, and it is a valuable lesson in packaging for real audiences, not just for oneself.

That exercise, completed for a small project, gives you a packaged project you can build on, a checklist you can reuse, and confidence that you can package future projects correctly. It also gives you a concrete artefact to show, whether for a portfolio, a course, or a team, demonstrating that you can take code from a script to a distributable package. That progression, from script to command-line tool to package, is the arc of the last three chapters, and it is a significant milestone in practical Python development.

Check: What does sharing a packaged project with another person reveal that self-testing may miss?

Section 50

This chapter has covered packaging from first principles to maintenance, and its lessons can be carried into every future project you share. You have learned how to organise a project, declare metadata, version releases, declare dependencies, build source and wheel distributions, define entry points, test in clean environments, write readmes and changelogs, choose licences, and handle publishing, both public and private. Those topics, taken together, form a complete, if introductory, picture of packaging, sufficient to package a real project with confidence, and to learn more advanced topics as needed.

The central message of this chapter is that packaging is not an afterthought, but a part of programming itself. Code that cannot be installed reliably, that lacks clear metadata, or that has no documentation, is code that cannot be used reliably by others, no matter how clever it is. Packaging, done well, respects users' time, by making installation predictable, by documenting use clearly, and by maintaining compatibility and updates thoughtfully. That respect, expressed through process, is what turns a personal script into a tool that others can depend on.

Looking ahead, packaging skills will interact with many topics in later chapters. Type hints and static checking, covered later, can be part of a package's quality checks. Documentation and docstrings, also covered later, complement the readme and changelog discussed here. Clean code and refactoring, covered later, make a package's code easier to maintain alongside its packaging. Those connections show that packaging is not isolated; it is one part of a larger practice of writing software that is clear, reliable, and shareable, and the habits from this chapter support those later topics.

As with previous chapters, the best consolidation is practice. Package a real project, test it cleanly, share it with someone, and maintain it through at least one update, with a changelog entry and a version bump. That experience, with its inevitable surprises and fixes, will teach you more than any further reading, because it will confront you with the details that only real distribution reveals. Keep the checklist from this chapter, update it with lessons you learn, and reuse it for future projects, so that each package you share is better than the last.

The next chapters move from sharing code to understanding how it runs: concurrency, security, performance, and the design principles that make code robust and maintainable. Those topics will deepen your understanding of Python itself, and they will, in turn, inform how you package and share the more sophisticated programs you will build. Carry the packaging habits from this chapter forward, and they will serve you well, whatever kind of program you write next.

A well-packaged project also makes collaboration easier, because new contributors can install it, run its tests, and understand its structure without needing extensive guidance from the original author.

Check: Why is packaging described as part of programming itself, not an afterthought?

Practical Task

Package a small command-line tool you have already written, or write a tiny one for this task. Create its project layout, metadata file, readme, licence file, and changelog, define an entry point, build its source and wheel distributions, inspect their contents, and install the built package in a fresh virtual environment. Test its command, its help output, and its main feature in that clean environment, and record any packaging mistake you found and fixed. Finally, write a short note explaining one decision you made about its name, version, or dependencies.

Chapter Questions

  1. Explain the difference between a source distribution and a wheel, and when each is most useful.
  2. Why must a package be tested in a clean environment, and what kinds of mistakes does that testing reveal?
  3. Describe a release process for a small project, including versioning, changelog, building, testing, and sharing.
  4. How should a library's packaging differ from an application's packaging, particularly regarding dependencies?
  5. A colleague installs your package but it fails to import, although it works on your computer. Diagnose the likely packaging causes in order.