Skip to content
Back home

Tutorial — Beginner to intermediate

Publish a Python CLI to PyPI

Package a Python command-line tool with pyproject.toml, test it, and publish it to PyPI with trusted publishing, using the real setup behind djentgen.
Reading time
35 min
Stack
Python 3.9+ · pyproject.toml · PyPI · GitHub Actions
Published
3 November 2026

I published my djent MIDI generator to PyPI as djenerator. At least, that was the plan. The name was already taken by an unrelated Django package for generating test data, so pip install djenerator gave people something completely different. The project became djentgen, and I learned to check names before anything else.

This tutorial is everything I'd tell myself before publishing that first Python package. We'll build notelen, a small CLI for musicians and producers that prints note lengths and delay times in milliseconds for any tempo:

$ notelen 120
note    straight    dotted   triplet   (120 bpm)
1/1       2000.0    3000.0    1333.3
1/2       1000.0    1500.0     666.7
1/4        500.0     750.0     333.3
1/8        250.0     375.0     166.7
1/16       125.0     187.5      83.3
1/32        62.5      93.8      41.7

It's small enough to read in one sitting, but the packaging setup is exactly what I use for djentgen.

Step 0: check the name

Do this before writing any code. Open https://pypi.org/project/your-name/ in a browser, or check from a terminal:

curl -s -o /dev/null -w "%{http_code}\n" https://pypi.org/pypi/notelen/json

A 404 means the name is free. A 200 means it's taken.

PyPI normalises names: case, hyphens, underscores and dots are all treated the same. So My_Tool, my-tool and my.tool are one name. If any of those exist, yours is taken. PyPI also blocks names that are too similar to popular packages, to stop typo-squatting.

Pick a name that's easy to type. People install your package from memory. djentgen is fine; djent-midi-pattern-generator-py isn't.

Step 1: one file to describe the project

Old Python packages used setup.py, setup.cfg, MANIFEST.in and sometimes requirements.txt all at once. Modern packaging needs one file: pyproject.toml. It tells tools two things:

  • How to build the package (the [build-system] table)
  • What the package is (the [project] table: name, version, dependencies, commands)

Every modern tool reads it: pip, build, uv, Poetry, Hatch and PyPI itself.

djentgen still has a setup.py, but only as a two-line shim for old tools. All its real metadata lives in pyproject.toml.

Step 2: choose a layout

There are two common layouts.

Flat layout, where the package sits at the project root. This is what djentgen uses:

djentgen/
  djentgen/
    __init__.py
    generator.py
  pyproject.toml

src layout, where the package lives inside a src/ folder:

notelen/
  src/
    notelen/
      __init__.py
  tests/
  pyproject.toml

We'll use the src layout for notelen, and it's what I'd recommend for new projects. With a flat layout, running your tests from the project root can import your local files directly, even if your packaging is broken. With src layout, Python can only import the package once it's installed, so your tests check what users will actually get.

Step 3: set up the project

Create the folders and a virtual environment:

mkdir -p notelen/src/notelen notelen/tests
cd notelen
python3 -m venv .venv
source .venv/bin/activate

Always work inside a virtual environment. It keeps this project's dependencies separate from everything else on your machine, and it means "it works on my machine" also means "it works in a clean environment".

Step 4: create your PyPI accounts

You'll need two accounts, both free:

  • PyPI, the real index
  • TestPyPI, a separate sandbox for practice uploads

They're completely separate, so you register twice. Both require two-factor authentication before you can publish.

What's next

You now have a free name, an empty project and somewhere to publish it. In the full tutorial we'll build and ship notelen:

  • the complete CLI and its pyproject.toml, line by line
  • a notelen command, plus python -m notelen support
  • a version number defined in exactly one place
  • tests with pytest
  • building and inspecting wheels and source distributions
  • a practice run on TestPyPI
  • publishing to PyPI from GitHub Actions with trusted publishing, so there are no API tokens to leak

Checking access…

More tutorials