Python Projects, Libraries, and Packages: What They Are and How to Build Them
A project, a library, and a package are three distinct things in Python. Understanding the difference changes how you structure your code and whether others can install and use it.
The words project, library, and package are used interchangeably in conversation but they mean different things in Python. A project is an application you run. A library is code you write for others to import. A package is the distributable unit that gets published to PyPI and installed with pip. Knowing which one you are building determines how you structure your code, what files you need, and what tooling applies.
A Normal Python Project
A project is code you intend to run, not distribute. A web application, a data pipeline, a script that automates something. It has dependencies, it has an entry point, and the only people who need to install it are the people running it.
The structure of a normal project:
my-project/
├── pyproject.toml
├── poetry.lock # or uv.lock
├── README.md
├── src/
│ └── my_project/
│ ├── __init__.py
│ └── main.py
└── tests/
└── test_main.py
The src/ layout places your code one level below the root. This is intentional. It prevents Python from accidentally importing your local source files instead of the installed package during testing, which masks import errors that would surface in production.
pyproject.toml is the single configuration file for modern Python projects. With Poetry it looks like this:
[tool.poetry]
name = "my-project"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.31"
[tool.poetry.dev-dependencies]
pytest = "^7.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
This project is not published anywhere. You clone it, run poetry install, and run it. That is the complete lifecycle.
A Library
A library is code you write for other developers to import into their own projects. It does not run on its own. It provides functions, classes, or utilities that are useful in other contexts.
The distinction from a project is architectural. A project has an entry point, a command you run. A library has a public API, functions and classes that other code calls. A library also has stricter constraints on its dependencies. A project can pin exact versions because it runs in a controlled environment. A library must declare loose version ranges because it will be installed alongside whatever other packages the consuming project uses, and those packages have their own version requirements.
[tool.poetry.dependencies]
python = "^3.10"
requests = ">=2.28,<3.0" # loose range, not pinned
A library committed to exact pinned versions will cause dependency resolution failures for anyone who tries to use it alongside other packages.
A Package
A package is a library that has been built into a distributable format and published to PyPI so that anyone can install it with pip install your-package-name. The word package specifically refers to the distributable artifact, not the code itself.
The structure mirrors a library but includes the files required for PyPI:
my-library/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── my_library/
│ ├── __init__.py
│ └── core.py
└── tests/
pyproject.toml for a publishable package requires additional metadata:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-library"
version = "0.1.0"
description = "A short description of what this does"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "Your Name", email = "you@example.com" }
]
dependencies = [
"requests>=2.28"
]
[project.urls]
Homepage = "https://github.com/you/my-library"
The name field must be unique on PyPI. Check that the name is available before building.
Building and Publishing
Install the build tool and twine:
pipx install build
pipx install twine
Build the package. This produces a .whl file (the built distribution) and a .tar.gz file (the source distribution) inside a dist/ directory:
python -m build
Before publishing to PyPI, test against TestPyPI to verify the upload works and the metadata looks correct:
twine upload --repository testpypi dist/*
Then install from TestPyPI to verify the package installs and imports correctly:
pip install --index-url https://test.pypi.org/simple/ my-library
When everything looks correct, publish to PyPI:
twine upload dist/*
You will need an API token from your PyPI account. Store it in ~/.pypirc or pass it as an environment variable rather than entering it interactively each time.
The Practical Difference in Summary
| Project | Library | Package | |
|---|---|---|---|
| Purpose | You run it | Others import it | Others install it |
| Entry point | Yes | No | Optional |
| Dependency pinning | Exact versions | Loose ranges | Loose ranges |
| Published to PyPI | No | No | Yes |
| Has lockfile | Yes | No | No |
A library becomes a package the moment you build and publish it. Until then it is just code organised for reuse.
What You Can Do Now
If you have a utility module you copy between projects, that is a library waiting to be a package. Start there:
# Create the structure
mkdir -p my-library/src/my_library
touch my-library/src/my_library/__init__.py
touch my-library/pyproject.toml
touch my-library/README.md
touch my-library/LICENSE
# Build it
cd my-library
python -m build
# Upload to TestPyPI first
twine upload --repository testpypi dist/*
Once it is on TestPyPI and installs correctly, publishing to PyPI is one command away.