Creating and publishing your first Python package
This post describes a step-by-step template for how to go about creating a Python project using uv and Github and publishing the end result as a package on PyPi.
As a example we create a simple Python package (< 200 lines of code) for finding and removing Personally Identifiable Information (PII) from text. I have published the result as blankit. Call your version whatever you like.
I will go through the following steps:
- Using the uv Python package manager to initialise a project
- Creating a Github repository for the project
- Incorporating unit tests
- Automating tests in different deployment environments using Github Actions
- Producing automated documentation from the codebase using sphinx
- Building and publishing the package to PyPi.
1. Project structure
I have recently switched to uv as an alternative to poetry as my preferred package manager. In other instances I have used conda but uv addresses all my issues with both of these. It is extremely fast at resolving package dependencies and convenient for managing projects. One caveat is that it is VC backed so, while it is currently free and open source, that may well change.
Firstly, install uv by following the instructions on the uv website.
Now you can easily set up a project in the command line via
uv init --package project_name
This will create a uv project with the following basic structure
project_name
|--- .python-version
|--- README.md
|--- pyproject.toml
|___ src
|___ project_name
|___ __init__.py
We get a README.md
(for giving users a basic overview of our project), a pyproject.toml
(for listing all the dependencies of your project), .python-version
that contains the python version and a src
folder for storing all the project code.
1.1. What is a pyproject.toml file?
From what I understand this is the modern approach for listing dependencies in a python package. Along with these dependencies the pyprofile.toml
file is also used to describe important metadata for your project, including the author name and contact details, project name, description and version. More details can be found in the Python packaging user guide.
Let’s modify this file to include some more information about our project.
[project]
name = "project-name"
version = "0.1.0"
description = "My PII detection project"
readme = "README.md"
authors = [
{ name = "Alex Lee", email = "ajlee3141@gmail.com" }
]
requires-python = ">=3.10"
dependencies = []
[project.scripts]
project-name = "project_name:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Now we can start adding dependencies.
1.2. Named Entity Recognition for PII detection
GLiNER is a widely used package for zero-shot named entity recognition. This just means that we can give a description of the categories that we are interested in detecting and, without any training, the model will do a pretty good job of detecting them. This makes it an effective out-of-the-box solution for PII detection since PII consists of explicit references to names, locations and other identifiers.
Let’s add this package to our project
uv add gliner
The pyproject.toml
file has now been updated with this additional dependencies
dependencies = [
"gliner>=0.2.19",
]
And we will also add a couple of other packages that will be useful later on:
uv add pandas blank ruff pytest
You will notice that uv has created a new file - uv.lock
- with all the resolved dependencies for these packages required by our project, and also a folder .venv
for our virtual environment.
If someone clones our project from a repo (see the next step), they can run uv run python
to install all the required dependences. In other words, there is no requirements.txt
file needed as for pip.
1.3. Write some code that solves our task
The final package contains two files: a utils.py
file and a scanner.py
file. The first contains functions for basic tasks like parsing and replacing the identified PII with generic PII labels. The second file implements a Scanner
class that provides an API for users to identify and redact a given string or list of strings. You can create these files in the src
folder and copy the code across from the blankit repo.
2. Create a Github repository for our project
We now have the key ingredients for our project. Let’s create a respository for our project on Github.
2.1. Initialise the github repo locally
Firstly we intialise git (assuming you already have it installed). This is done in the base folder of the project:
git init
This will create: a file called .gitignore
that tells the repo which files should not be tracked by git when we make a push request; and a folder named .git
for tracking changes.
If you’re not comfortable with git there are plenty of tutorials online
Now we can add our changes and make our initial commit.
git add .
git commit -m "Initial commit"
2.2. Create a github respository in your Github account and push your code
Now log in to your Github account and create a repository. Once that is created, you can push all the current project code to this repo.
git remote add origin https://github.com/yourusername/your-repo-name.git
git branch -M main
git push -u origin main
3. Add some unit tests
We are still missing a couple of things, one of which is some unit tests. This helps us to check that when we make changes to our code, we don’t break other things. We will add a couple of these, to check the functionality of our utility functions.
Create a folder tests
under the project parent directory and a file test_function.py
in this folder
def test_basic():
assert len("mango") == 5
We will use pytest to run tests and so we add a few lines to our pyproject.toml
file to specify where to find the test functions
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = "test_*"
Now we can run pytest as follows
uv run pytest
You should see that 2 of out 2 tests have passed (assuming nothing broke in the code). You can also commit these and push to your git repo
git add tests
git add pyproject.toml
git commit -m 'Update with tests'
git push
4. Automation with Github Actions
We are almost there. One useful addition to our project is to run a series our tests each time we commit to Github, so that anything that breaks can be detected before the code is pushed. In other words, it helps to avoid pushing broken code.
Also, since most users will not be working within our exact set up (operating system and python version), ensuring unit tests performed by Github in different environments (Windows, Mac, Linux) will reduce the chance that the users of the package will encounter bugs that you didn’t anticipate.
This can all be done via a Github Action.
We will add an action for carrying out tests. Create a file test.yml
under a new folder .github/workflows/
and include the following in the file
name: project_name
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install the project
run: uv sync --locked --all-extras --dev
- name: Run tests (Linux/macOS)
if: runner.os != 'Windows'
run: uv run pytest tests
Each time we push code to the repo this action will run, which installs uv and the project files and runs the tests in the tests folder. It will carry out these actions in a Mac OS and Ubuntu environment for three different Python versions.
There are a whole range of github actions. For this one I copied the code from the following:
5. Documentation
In addition to automated testing, it is handy to use automation to create documentation. This avoids the need to manually edit and update documentation each time you make a change. Instead, you can use a tool such as Sphinx and readthedocs to build the documentation from your code base. The documentation will be rebuilt whenever code is pushed to the repo.
Firstly, create a new folder under docs/source
called conf.py
:
# Configuration file for the Sphinx documentation builder.
import os
import sys
sys.path.insert(0, os.path.abspath('../..'))
# -- Project information
project = 'project_name'
copyright = '2025, Alex Lee'
author = 'Alex Lee'
release = '0.1.0'
version = "0.1.0"
# -- General configuration
extensions = [
'sphinx.ext.duration',
'sphinx.ext.doctest',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.intersphinx',
'sphinx.ext.napoleon',
]
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
}
intersphinx_disabled_domains = ['std']
# -- Options for HTML output
html_theme = 'alabaster'
# -- Options for EPUB output
epub_show_urls = 'footnote'
Also add a .readthedocs.yaml
file to the root directory of your repo:
version: "2"
sphinx:
configuration: docs/source/conf.py
build:
os: "ubuntu-22.04"
tools:
python: "3.10"
python:
install:
- method: pip
path: .
Read the Docs doesn’t yet support pyproject.toml files so we have to now create a requirements.txt
file. This is straightforward in uv:
uv pip compile pyproject.toml > docs/requirements.txt
Next log into Read The Docs and import your Github repo (you will need to first create an account on RTD if you haven’t).
Now, each time we push code to our repo, the documentation will be automatically built and available at https://project_name.readthedocs.io/en/latest/
6. Build and distribute the package
Now we are ready to build the package and publish it to make it available to the world. Firstly, create a distribution of the package:
uv build
This will create a .tar.gz
and .whl
file under the new dist
folder.
Let’s publish it to PyPI (note that you will need to setup a PyPI account with authentication to do this).
uv publish
The package is now listed on the python package index and can be installed via uv
, conda
, poetry
or pip
Summary
And that’s it! You can now create a project in uv, make it available on github, incorporate some automated testing and documentation creation and make your project available to the rest of the Python community. As a bonus we have an easy-to-use Python package for redacting PII from text, a pretty common task when working with sensitive data.