Packaging a Python Project
Structure a distributable package, write pyproject.toml, install it locally for development, and publish it to PyPI.
"A package is code you can share. Structure your project correctly once, write a pyproject.toml, and anyone on Earth can install your work with a single pip install command."
What Makes a Python Package?
Any directory containing an __init__.py file is a Python package. That file can be empty — its presence is what tells Python "this directory is a package that can be imported":
my_project/ ├── src/ │ └── mylib/ │ ├── __init__.py ← makes it a package │ ├── core.py ← main logic │ └── utils.py ← helpers ├── tests/ │ ├── test_core.py │ └── test_utils.py ├── pyproject.toml ← project metadata + build config └── README.md
__init__.py — Your Package’s Public API
What you put in __init__.py is what users get when they import your package. Think of it as the front door:
# Import the things you want users to access directly
from .core import calculate, process
from .utils import format_result
__version__ = "0.1.0"
__author__ = "Riya Sharma"
# __all__ controls what "from mylib import *" exposes
__all__ = ["calculate", "process", "format_result"]
from mylib import calculate # works because of __init__.py
import mylib
print(mylib.__version__) # "0.1.0"
pyproject.toml — Modern Project Configuration
The single file that defines everything about your project — its name, version, dependencies, and how to build it:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "mylib" # pip install mylib
version = "0.1.0"
description = "A useful Python library"
authors = [{name = "Riya Sharma", email = "riya@example.com"}]
readme = "README.md" # shown on PyPI
requires-python = ">=3.10"
license = {text = "MIT"}
# These get pip-installed automatically when someone installs mylib
dependencies = [
"requests>=2.31",
"click>=8.0",
]
# pip install mylib[dev] — installs dev tools too
[project.optional-dependencies]
dev = ["pytest", "black", "mypy"]
# Creates a terminal command: mylib-cli → calls mylib.cli:main()
[project.scripts]
mylib-cli = "mylib.cli:main"
Install Locally for Development
# Install in editable mode — source changes are reflected instantly
# Run this ONCE from your project root directory
pip install -e .
# Now you can import your package anywhere on your machine:
python -c "from mylib import calculate; print('Works!')"
# And update: just edit the source file — no reinstall needed
Build and Publish to PyPI
pip install build twine
python -m build
dist/mylib-0.1.0.tar.gz and dist/mylib-0.1.0-py3-none-any.whltwine upload dist/*
pip install mylibpip install build twine
python -m build
# Successfully built mylib-0.1.0.tar.gz and mylib-0.1.0-py3-none-any.whl
twine upload dist/*
# Uploading distributions to https://upload.pypi.org/legacy/
# Uploading mylib-0.1.0-py3-none-any.whl
# View at: https://pypi.org/project/mylib/
PyPI has a test server at test.pypi.org — publish there first with twine upload --repository testpypi dist/* and install with pip install --index-url https://test.pypi.org/simple/ mylib. Confirm everything works before pushing to the real PyPI.
"pip install -e . is the single most useful command during development. It installs your package in editable mode — you edit a source file and the change is live immediately, no reinstall needed."
— Shurai🧠 Quiz — Q1
What makes a directory a Python package that can be imported?
🧠 Quiz — Q2
What is the purpose of pyproject.toml?
🧠 Quiz — Q3
What does pip install -e . do, and why is it useful for development?
🧠 Quiz — Q4
After running python -m build, what files are created?