The Coding Lab
<- All posts

Packaging a Python CLI with Console Scripts - 5/9

Packaging a Python CLI with setup.py, a src layout, dependencies, and an installable console entry point.

The Bash installer worked by encoding one installation procedure. Python packaging uses a stronger abstraction: the project declares its metadata, dependencies, build backend, and command entry points, while compatible tools decide how to build and install it.

This moves us from “run my script” to “install this Python distribution.” The distinction matters because the distribution has a name, a version, dependency requirements, and a standard artifact format.

Series

  1. From a Script to a Command-Line Tool
  2. When sys.argv Stops Scaling
  3. Designing a CLI with argparse
  4. Why a Bash Installer Becomes a Liability
  5. Packaging a Python CLI with Console Scripts
  6. Testing a Python CLI and Automating CI
  7. Documentation as Part of the Product
  8. Building a Reliable PyPI Release Pipeline
  9. Publishing a Pure-Python CLI with Conda

Give the project a package layout

We will move the CLI code into an importable package under src:

tinyhttpie/
├── README.md
├── setup.py
├── src/
│   └── tihttp/
│       ├── __init__.py
│       └── cli.py
└── tests/

The src layout prevents the repository root from being imported accidentally during development. Tests exercise the installed package, which catches missing-package and installation mistakes earlier.

The build_parser(), main(), and run() functions from part 3 now live in src/tihttp/cli.py.

Describe the package with setup.py

The setup file is the package’s contract with setuptools and pip. It combines descriptive metadata with the information needed to discover, install, and invoke the application:

from pathlib import Path

from setuptools import find_packages, setup


setup(
    name="tihttp",
    version="0.1.0",
    description="A small command-line HTTP client",
    long_description=Path("README.md").read_text(encoding="utf-8"),
    long_description_content_type="text/markdown",
    license="MIT",
    python_requires=">=3.6",
    package_dir={"": "src"},
    packages=find_packages(where="src"),
    install_requires=[
        "requests>=2.21",
    ],
    extras_require={
        "dev": ["pytest", "tox", "twine", "wheel"],
        "docs": ["sphinx", "sphinx-rtd-theme"],
    },
    entry_points={
        "console_scripts": [
            "tihttp=tihttp.cli:run",
        ],
    },
)

These fields describe different contracts:

  • python_requires prevents installation on unsupported interpreters.
  • package_dir and find_packages() tell setuptools where the importable code lives.
  • install_requires lists dependencies needed when the command runs.
  • extras_require groups development and documentation tools without installing them for every user.
  • console_scripts maps the public tihttp command to the run() function.

Because setup.py is executable Python, it is tempting to place application logic in it. Keeping it declarative and side-effect free makes builds easier to understand and reproduce.

The entry point is the important bridge. Users no longer need to know the source filename, module layout, interpreter path, or virtual-environment location. The installer generates the platform-appropriate launcher.

An entry point split into command, package, module, and function

An entry point connects the command name to a package, module, and function.

Install for use or development

A regular installation copies the package into the active environment and installs its dependencies and command:

$ python -m pip install .
$ tihttp --help

During development, an editable install keeps imports and the console script connected to the working tree:

$ python -m pip install --editable '.[dev]'

Editable installation is a development convenience, not a different release artifact. A release should still be built and tested in the form users will install.

Build standard distribution artifacts

Setuptools can create a source distribution, while the wheel package adds the wheel build command:

$ python -m pip install wheel
$ python setup.py sdist bdist_wheel

Successfully built tihttp-0.1.0.tar.gz and tihttp-0.1.0-py3-none-any.whl

The source distribution contains the material needed to build the project. The wheel is an installable archive and avoids running the project’s build process on the user’s machine.

This separation is one of the strengths of the Python ecosystem: project metadata, build frontend, build backend, installer, and package index are distinct pieces with documented interfaces. A small project can use the simple path without inventing its own distribution protocol.

Next, we need evidence that the installed command behaves correctly across supported environments. Part 6 adds tests and continuous integration.

Further reading

<< section 4 | section 6 >>