Skip to Content
Course content

345: Publishing a Package to PyPI

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've got this little utility script I've been using for months called title-scrubber. It does one thing: it takes a messy news headline—you know, the ones with "[BREAKING]" or "(EXCLUSIVE)" tacked onto the front—and cleans them up for a database. I'm tired of copying the scrubber.py file into every new project I start. It's time to just put it on PyPI so I can pip install it and move on with my life.

Why can't I just import this?

Right now, my code is just sitting in a folder. If I try to import it from another directory, Python tells me ModuleNotFoundError. I could mess around with sys.path, but that's a hack. To make this a real package, I need to structure it properly. I'll move the logic into a directory called title_scrubber and add an __init__.py file so Python treats the directory as a package.


title-scrubber/
├── pyproject.toml
└── src/
    └── title_scrubber/
        ├── __init__.py
        └── cleaner.py

I've put the actual cleaning logic in cleaner.py. But if I try to run pip install . from the root, it might work locally, but it's not "packaged." It's just a local link. If I want other people (or my future self on a different machine) to use it, I need a blueprint.

The missing blueprint

Back in the day, we used setup.py for everything. Now, the standard is pyproject.toml. It's cleaner and doesn't require executing a Python script just to find out what the package metadata is. I'll create one now.

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "title-scrubber-yourname" # I'll add my name so it doesn't clash with existing packages
version = "0.1.0"
authors = [
  { name="Your Name", email="you@example.com" },
]
description = "A small utility to strip junk from news headlines"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

I noticed I forgot the README.md. PyPI uses that for the project home page, and if it's missing, the upload might complain or just look empty. I'll whip up a quick markdown file explaining how to use title_scrubber.clean().

Baking the wheel

Now I have the code and the blueprint, but PyPI doesn't take raw source folders. It wants "distributions"—specifically source distributions (sdist) and wheels. I'll use the build module for this.

pip install build
python -m build

I see a new dist/ folder appeared. Inside, there's a .tar.gz file and a .whl file. The wheel is the "built" version that makes installation fast for the end user. I'm tempted to just upload these now, but I've made mistakes before—like typos in the version number—and once something is on PyPI, you can't "delete" a version; you can only yank it, which is a mess.

The TestPyPI safety net

I'm going to use TestPyPI first. It's a separate instance of the package index specifically for testing. To upload, I'll use twine, which is the industry standard for securely pushing packages.

pip install twine
python -m twine upload --repository testpypi dist/*

It asked for a username and password. I realized quickly that PyPI now heavily pushes API tokens for security. I'll head to my account settings, generate an "API Token," and use __token__ as the username and the token string as the password. Much better than putting my main password in a terminal.

Now I can try installing it from the test server: pip install --index-url https://test.pypi.org/simple/ title-scrubber-yourname. It works. The headlines are getting scrubbed, and the versioning is correct.

Going live on the real PyPI

The final jump is easy. Since the TestPyPI run was successful, I just run the upload command again, but this time I omit the --repository testpypi flag to target the production server.

python -m twine upload dist/*

And that's it. I can now go to any machine, run pip install title-scrubber-yourname, and my utility is right there. It feels a bit overkill for a 20-line script, but the workflow is the same whether it's a tiny utility or the next big data science library.




📋 Practical Task

Publishing the "Slugify-Pro" Utility

Your task is to package and prepare a small utility called slugify-pro for publication. This utility should take a string and convert it into a URL-friendly slug (e.g., "Hello World! This is Python" becomes "hello-world-this-is-python").

  • The Code: Create a directory structure with src/slugify_pro/ containing an __init__.py and a slugifier.py file that implements the slugging logic.
  • The Blueprint: Create a pyproject.toml file. Ensure you give the package a unique name (e.g., slugify-pro-yourusername) to avoid naming conflicts on the public index.
  • The Build: Use the build module to generate the dist/ folder containing the wheel and source distribution.
  • The Verification: Use twine check dist/* to verify that your package description and metadata are valid before attempting any upload.

Note: You do not need to actually upload to the real PyPI for this exercise, but you must provide the pyproject.toml and the command sequence you would use to move from code to the final dist/ folder.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.