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.tomlconfigured and ready - A package built and published to PyPI
- A working
pip install tinyfiledbyou 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
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 undersrc/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 inpip install. Must be unique on PyPI; use hyphens by convention. -
version- Semantic versioning; start at0.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 aLICENSEfile. -
authors- Name/email; shown on PyPI. -
requires-python- We usedict | Nonein 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 withpypi-); 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
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:
tinyfiledb
A lightweight file-based database for Python projects. No server, no migrations, no dependencies — just a local JSON file and a simple API.
If this project was useful to you, consider donating to support the Developer Service Blog: https://buy.stripe.com/bIYdTrggi5lZamkdQW
Install
pip install tinyfiledb
Usage
from tinyfiledb import FileDB
db = FileDB("mydata.json")
# Insert a record
user_id = db.insert({"name": "Alice", "role": "admin"})
# Get, update, list, delete
print(db.get(user_id))
db.update(user_id, {"role": "superadmin"})
print(db.all())
db.delete(user_id)
API
-
insert(record)— add a record, get back an auto-generated ID -
get(id)— retrieve one record by ID (returnsNoneif not found) -
all()— return every record as a dict -
update(id, data)— merge data into an existing record; returns…
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.
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
Top comments (0)