How to Build and Publish a Python Package to PyPI (With a Real Project)

By Nuno Bispo
9 min read

Table of Contents

You've written a useful Python utility, a helper for parsing files, a small data tool, or a class you keep copying between projects. At some point you think: I wish I could just pip install this. You can, and it's simpler than it looks.

Packaging your code and publishing it to PyPI (the Python Package Index) forces you to treat your code as a product: clear interfaces, versioning, and documentation others can follow. A published package is also a credibility signal, you didn't just write code, you shipped it.

In this tutorial we'll build tinyfiledb, a lightweight file-based database that stores, retrieves, updates, and deletes records in a local JSON file. No server, no migrations, no dependencies. It's small by design so we can focus on the packaging workflow.

By the end you will have:

  • A properly structured Python package
  • A pyproject.toml configured and ready
  • A package built and published to PyPI
  • A working pip install tinyfiledb you can share

The Project: A Tiny File Database

tinyfiledb is a minimalist file-based database: no server, no ORM. It stores records as JSON and exposes a simple Python API:

  • insert(record) - add a record, get back an auto-generated ID
  • get(id) - retrieve one record by ID
  • all() - return every record
  • update(id, data) - merge data into an existing record
  • delete(id) - remove a record
Creating Your Own NoSQL Database in Python
This article will walk you through the basic steps to create a simple, yet working, NoSQL database in Python.

Want to take this further? Here's how to add query capabilities and build a fuller NoSQL layer.

Project Structure

This is the project structure you will use:

tinyfiledb/
├── tinyfiledb/
│   ├── __init__.py
│   └── db.py
├── pyproject.toml
└── README.md

tinyfiledb/db.py

The main project file that contains the tiny database implementation:

import json
import uuid
from pathlib import Path


class FileDB:
    def __init__(self, filepath: str = "db.json"):
        self.path = Path(filepath)
        if not self.path.exists():
            self._write({})

    def _read(self) -> dict:
        with open(self.path, "r") as f:
            return json.load(f)

    def _write(self, data: dict):
        with open(self.path, "w") as f:
            json.dump(data, f, indent=2)

    def insert(self, record: dict) -> str:
        data = self._read()
        record_id = str(uuid.uuid4())[:8]
        data[record_id] = record
        self._write(data)
        return record_id

    def get(self, record_id: str) -> dict | None:
        return self._read().get(record_id)

    def all(self) -> dict:
        return self._read()

    def update(self, record_id: str, new_data: dict) -> bool:
        data = self._read()
        if record_id not in data:
            return False
        data[record_id].update(new_data)
        self._write(data)
        return True

    def delete(self, record_id: str) -> bool:
        data = self._read()
        if record_id not in data:
            return False
        del data[record_id]
        self._write(data)
        return True

Supports full CRUD operations - insert() returns an auto-generated 8-character UUID key, get() and all() read from disk on every call, and update() merges new fields into an existing record rather than replacing it. The file is created automatically on initialization if it doesn't exist.

tinyfiledb/__init__.py

Expose only what users need:

from .db import FileDB

__all__ = ["FileDB"]

Quick Usage

You can test it with this quick usage example:

from tinyfiledb import FileDB

db = FileDB("mydata.json")
user_id = db.insert({"name": "Alice", "role": "admin"})
print(db.get(user_id))   # {'name': 'Alice', 'role': 'admin'}
db.update(user_id, {"role": "superadmin"})
print(db.all())
db.delete(user_id)

Should return:

{'name': 'Alice', 'role': 'admin'}
{'daf0f9fb': {'name': 'Alice', 'role': 'superadmin'}}

Roughly 50 lines of code - enough to be useful, small enough to keep the focus on packaging.


Structuring Your Package

Your project must be laid out so Python's tooling can find and install the package. There are two common layouts:

  • src/ layout - package lives under src/tinyfiledb/. Helps avoid importing the local uninstalled copy during tests.
  • Flat layout - package sits directly in the project root as tinyfiledb/. Simpler and what we'll use.

The project uses the flat layout:

tinyfiledb/
├── tinyfiledb/
│   ├── __init__.py
│   └── db.py
├── pyproject.toml
└── README.md

Your __init__.py is the package's public face.

Keep it minimal: re-export the main API and set __all__ so the public surface is explicit.

We already did that above.


Writing pyproject.toml

setup.py required running Python just to read metadata. pyproject.toml is a static config file, no code execution. Every modern build tool (Hatchling, Flit, setuptools) and PyPI expect it.

For new packages, it's the only config you need. Create pyproject.toml in the project root:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "tinyfiledb"
version = "0.1.0"
description = "A lightweight file-based database for Python projects."
readme = "README.md"
license = { text = "MIT" }
authors = [
  { name = "Your Name", email = "you@example.com" }
]
requires-python = ">=3.10"
dependencies = []

What each part does:

  • [build-system] - Tells Python how to build the package. Hatchling is fast and needs no extra config for a simple project.
  • name - What users type in pip install. Must be unique on PyPI; use hyphens by convention.
  • version - Semantic versioning; start at 0.1.0.
  • description - One-line summary on PyPI; keep it short.
  • readme - PyPI uses this for the project description.
  • license - e.g. { text = "MIT" } or reference a LICENSE file.
  • authors - Name/email; shown on PyPI.
  • requires-python - We use dict | None in code, so >=3.10.
  • dependencies - Runtime dependencies; empty for tinyfiledb.

Building the Distribution

A distribution is a versioned snapshot of your package that can be uploaded and installed. Python uses two formats; you'll produce both.

Install the build tool

pip install build

Run the build

From the directory that contains pyproject.toml:

python -m build

You'll get:

* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for wheel...
* Building wheel...
Successfully built tinyfiledb-0.1.0.tar.gz and tinyfiledb-0.1.0-py3-none-any.whl

A dist/ folder will appear:

  • .tar.gz - Source distribution (sdist): raw source + metadata. Pip can build from it when no wheel is available.
  • .whl - Wheel: pre-built; pip installs it directly with no build step.

Publishing to PyPI

Use TestPyPI first.

PyPI does not allow re-uploading the same version. If you push 0.1.0 with a mistake, you must release 0.1.1.

TestPyPI is a sandbox - upload and fix without burning versions.

Install twine (upload tool):

pip install twine

TestPyPI

First make sure your account and token are ready:

  • Account - test.pypi.org/account/register. Verify your email.
  • 2FA - PyPI requires two-factor authentication for uploads. Enable it under Account Settings before creating a token.
  • API token - Account Settings → API Tokens → Add. Name it (e.g. tinyfiledb-upload), copy the token (starts with pypi-); you won't see it again.

Upload:

twine upload --repository testpypi dist/*

When prompted: username __token__, password = your token (literally __token__, not a placeholder).

Uploading distributions to https://test.pypi.org/legacy/
WARNING  This environment is not supported for trusted publishing
Enter your API token: 
Uploading tinyfiledb-0.1.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.8/4.8 kB • 00:01 • ?
Uploading tinyfiledb-0.1.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.4/4.4 kB • 00:00 • ?

View at:
https://test.pypi.org/project/tinyfiledb/0.1.0/

Test install in a fresh venv:

python -m venv test-env
# Windows: test-env\Scripts\activate
# macOS/Linux: source test-env/bin/activate

pip install --index-url https://test.pypi.org/simple/ tinyfiledb

Then:

python -c "from tinyfiledb import FileDB; db = FileDB('t.json'); print(db.insert({'x': 1}))"

If that works, you're ready for PyPI.

PyPI (production)

Make sure your account and token are ready:

  • Account - pypi.org/account/register. Verify email.
  • 2FA - PyPI requires two-factor authentication for uploads. Enable it under Account Settings before creating a token.
  • API token - Account Settings → API Tokens → Add. Use a project-scoped token after the first upload if you prefer.

Upload:

twine upload dist/*

Username: __token__, password: your PyPI token. After a successful upload you'll get a project URL. Your package is live.

Uploading distributions to https://upload.pypi.org/legacy/
WARNING  This environment is not supported for trusted publishing
Enter your API token: 
Uploading tinyfiledb-0.1.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.8/4.8 kB • 00:00 • ?
Uploading tinyfiledb-0.1.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.4/4.4 kB • 00:00 • ?

View at:
https://pypi.org/project/tinyfiledb/0.1.0/

Anyone can install it with:

pip install tinyfiledb

My Experience Building a URL Shortener Like TinyURL
I built my own TinyURL-style URL shortener to explore how these systems work under the hood. From generating short codes to handling redirects and storage, this article covers my hands-on experience, the design decisions I made, and what I learned from building a URL shortener from scratch.

Another small Python project that went through the full build-and-ship cycle.


Installing and Using Your Package

Verify in a clean environment (no local copy of the code):

mkdir demo && cd demo
python -m venv env
# Windows: env\Scripts\activate
# macOS/Linux: source env/bin/activate

pip install tinyfiledb

Check metadata:

pip show tinyfiledb

Will show:

Name: tinyfiledb
Version: 0.1.0
Summary: A lightweight file-based database for Python projects.
Home-page:
Author:
Author-email: Your Name <you@example.com>
License: MIT
Location: D:\GitHub\tinyfiledb\demo\env\Lib\site-packages
Requires:
Required-by:

You can do a quick test:

from tinyfiledb import FileDB

db = FileDB("tasks.json")
id1 = db.insert({"title": "Write blog post", "done": False})
id2 = db.insert({"title": "Publish to PyPI", "done": True})
print(db.get(id1))
print(db.all())
db.update(id1, {"done": True})
db.delete(id2)

Which returns:

{'title': 'Write blog post', 'done': False}
{'4d62fb2a': {'title': 'Write blog post', 'done': False}, '0f43c5be': {'title': 'Publish to PyPI', 'done': True}}

After running, open tasks.json - you'll see human-readable JSON:

{
  "4d62fb2a": {
    "title": "Write blog post",
    "done": true
  }
}

That's the whole flow: from a folder on your machine to a package anyone can install with one command.

Full source code available at:

GitHub - nunombispo/tinyfiledb
Contribute to nunombispo/tinyfiledb development by creating an account on GitHub.

Conclusion

You built a real Python package: a small file-based database with a clear API, proper layout, and pyproject.toml.

You built distributions with python -m build, tested on TestPyPI with twine, then published to PyPI. The same workflow applies to any project you want to share - CLI tools, utilities, libraries.

The best first package is often something you've already written, a module you've copied between projects or a script you've shared. Add a pyproject.toml, a README, and a few minutes of setup, and it can be installable by anyone.

The ecosystem is built on packages that started that way. Yours doesn't need to be big; it just needs to exist.

Next steps worth exploring: semantic versioning, GitHub Actions for releases, pytest for tests, and project-scoped API tokens on PyPI.


Want to Go Deeper?

This article covers the full publish workflow, but there's a lot more ground to cover, structuring larger packages, writing a test suite, automating releases, handling versioning properly, and building a package others actually want to use.

pip install yours
Learn how to package and publish Python projects to PyPI. A hands-on guide covering pyproject.toml, build tools, versioning, and releasing open-source libraries.

If you want all of that in one place, pip install yours is the companion booklet to this tutorial. It picks up exactly where this article ends and walks you through everything it takes to build, maintain, and grow a Python package worth installing.

Now go ship something.


Follow me on Twitter: https://twitter.com/DevAsService

Follow me on Instagram: https://www.instagram.com/devasservice/

Follow me on TikTok: https://www.tiktok.com/@devasservice

Follow me on YouTube: https://www.youtube.com/@DevAsService

Tagged in:

Python, PyPi, package

Last Update: March 09, 2026

About the Author

Nuno Bispo Netherlands

Building better devs, one post at a time. 💻 Practical tips, pro insights, and tools that actually work. Read the blog today.

View All Posts