Package Structure and pyproject.toml
Create an installable project and verify the built artifact in a clean environment.
By the end, you’ll be able to…
- Distinguish an import package from a distribution.
- Declare build metadata and a console entry point.
- Test a wheel independently of the source checkout.
01Arrange code around its public interface
An import package is the name used in Python source, such as catalog_report. A distribution is the installed project described by packaging metadata, such as coursepodia-catalog-demo. Their names can differ. A src layout places the import package beneath src so running from the repository root does not accidentally make uninstalled application code importable.
Keep domain functions separate from command-line parsing and external adapters. Importing a package should define functions and classes, not launch a network request or start a CLI. Include a README with setup and usage, a license you are entitled to apply, and tests outside the import package. The small layout below gives these responsibilities visible homes without introducing unnecessary architectural layers.
catalog-project/ pyproject.toml README.md LICENSE src/ catalog_report/ __init__.py __main__.py cli.py core.py tests/ test_core.py test_cli.py02Declare the build and runtime contract
pyproject.toml identifies the build backend and the project metadata. Build requirements are tools needed to create the distribution; project dependencies are packages needed to run it. An application that uses only the standard library can have an empty dependency list even though its build backend is an external package. requires-python tells installers which interpreter versions the project supports.
A console-script entry point names an importable callable. Here catalog-report will call catalog_report.cli.main after installation. Keep that callable parameterless from the entry point’s perspective by using an optional argv argument for tests. Create the listed source files and README before building; a metadata configuration alone is not a working package. Versioning and compatibility promises should reflect what you actually test.
[build-system]requires = ["setuptools>=68"]build-backend = "setuptools.build_meta"[project]name = "coursepodia-catalog-demo"version = "0.1.0"description = "A small catalog reporting CLI"readme = "README.md"requires-python = ">=3.12"dependencies = [][project.scripts]catalog-report = "catalog_report.cli:main"[tool.setuptools.packages.find]where = ["src"]03Give each entry path one implementation
The cli module should expose main(argv=None) and return an integer exit status. A tiny __main__.py can import main and raise SystemExit(main()), enabling python -m catalog_report. The generated console script calls the same function, so the two entry paths share parsing and error behavior. Keep __init__.py lightweight to avoid unexpected work during import.
During development, an editable installation is convenient because imports refer to the working source. It does not prove the wheel contains every required file. Run the test suite against the development environment, build both a source distribution and wheel using a build frontend, and inspect the artifacts. Resource files and package discovery mistakes often appear only after installation from the built wheel.
from catalog_report.cli import mainif __name__ == '__main__': raise SystemExit(main())04Verify the artifact in isolation
In an activated development virtual environment, install the build frontend, run python -m build, and inspect dist. Then create and activate a separate clean virtual environment, install the wheel with --no-deps, move outside the source checkout, and run catalog-report --help. Verify that importing catalog_report resolves to the installed environment and that a representative fixture command produces the expected output.
Use the operating system’s appropriate activation command for the fresh environment. These installation steps are deliberately non-runnable in the lesson sandbox. Do not publish a package merely to test whether it builds. A release process adds version review, dependency policy, provenance, and publication authorization; the acceptance target here is a reproducible locally built artifact whose installed entry point and behavior have been checked.
# In the activated development environment:python -m pip install buildpython -m build# Create and activate a separate environment before the next step:python -m venv wheel-check# After activation, install the wheel and run from outside the checkout:python -m pip install --no-deps /absolute/path/to/dist/coursepodia_catalog_demo-0.1.0-py3-none-any.whlcatalog-report --helppython -c "import catalog_report; print(catalog_report.__file__)"The ideas worth keeping.
- Separate distribution metadata from import package structure.
- Keep entry points thin and imports free of orchestration.
- Build and smoke-test the wheel in a clean environment.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Parse the packaging metadata
Use tomllib to verify a minimal pyproject string has a Python requirement and a console entry point. No installation is needed.
Explore a worked solution
import tomllibmetadata = tomllib.loads('''[project]name = "demo"version = "0.1.0"requires-python = ">=3.12"[project.scripts]catalog-report = "catalog_report.cli:main"''')assert metadata['project']['requires-python'] == '>=3.12'assert metadata['project']['scripts']['catalog-report'] == 'catalog_report.cli:main'TOML parsing catches syntax errors and confirms structure. It does not replace building and installing the artifact.
Keep the entry point testable
Write main(argv=None) for a tiny argparse command with --version. Demonstrate testing the version output without reading the real process arguments.
Explore a worked solution
import argparseimport contextlibimport iodef main(argv=None): parser = argparse.ArgumentParser(prog='catalog-report') parser.add_argument('--version', action='version', version='0.1.0') parser.parse_args(argv) return 0output = io.StringIO()try: with contextlib.redirect_stdout(output): main(['--version'])except SystemExit as error: assert error.code == 0assert output.getvalue().strip() == '0.1.0'Optional argv supports a console entry point and direct tests. argparse handles the version action by printing and raising a successful SystemExit.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Parse the packaging metadata
Use tomllib to verify a minimal pyproject string has a Python requirement and a console entry point. No installation is needed.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.