The next question from every new Python programmer, right after "how do I print stuff," is:
"How do I make my awesome Python thingy available to other people?"
To answer that, we need to talk about the Python ecosystem and why packaging matters. Not the mechanics -- we'll get to those in separate articles on uv and Poetry. This is about the why.
In the days of yore, when you needed a library to extend the functionality of your program, you only had a few choices. You could write it yourself. Your operating system might provide it. Or -- best and worst of all -- you could look for it on the Internet.
Finding things on the early Internet was difficult. It took patience, hard-won experience, and luck. You had to follow your favorite topic in USENET news groups, exchange email with other developers, hang out in IRC chats, or hear about the latest thing while hacking out your semester project in the computer lab. Later, newfangled "web" servers started popping up using the untested and weird Hyper-Text Transfer Protocol. These "websites" soon proved their worth, becoming gateways to pre-existing but hard to find anonymous FTP and gopher sites as well as aggregating information on various esoteric topics.
If you found something close to what you needed, you then had to get it running on your particular magic combination of hardware, compiler, and operating system. The early Internet was not homogeneous -- there were lots of different flavors of Unix, VMS, OS/2, and many others now forgotten running on just as many different machines with varying architectures and capabilities.
The majority of software was written in C, which made porting easier but far from simple. You had to work around different services offered by different operating system flavors and different C compiler implementations. Not to mention the different capabilities offered by different hardware platforms -- not everybody had access to fancy new CD-ROM drives and had to make do with 3.5" floppy disks.
After lots of hacking and learning far more about your machine, OS, and compiler than you ever wanted, you finally have a compiled version of the library that you hope works.
"Testing? It compiled didn't it!? Ship it." - Embarrassingly, me.
You shim it into your project and get on with whatever it was you were trying to accomplish. You can see why "roll your own" was a viable option -- sometimes it was easier to just learn how to write the functionality you needed than to find someone else's software and get it running. That's how we got experts in cryptography, compression, networking, languages, and all the other things that make computers fun.
Finally, the nightmare scenario. Seven months later, after your program has been put into production and you have moved on to another project, a shadow falls across your desk. There's a bug and it stems from the library you worked so hard to incorporate. You again have choices: hope you can fix the library, hope the maintainer has fixed the bug and the source is still accessible, or hope the Internet has offered up a replacement in the interim.
Notice there is a lot of hoping there. Hope is not a plan.
Python as a language is friendly to new programmers while providing
enough depth and mystique to draw in hard-core nerds. But the real
strength of Python isn't the language -- it's the ecosystem that
allows us to flippantly say "Oh, you need a high performance web
server for your new RESTful API? Just pip install [gunicorn][gunicorn]."
The discovery story has improved since the fun-old-days of trawling
USENET. PyPI (the Python Package Index) is the central
registry -- hundreds of thousands of packages, searchable, versioned,
and installable with a single command. The tooling has evolved too:
where we once had just pip and setuptools, we now have modern
tools like Poetry and uv that handle dependency
resolution, virtual environments, and publishing in one coherent
workflow. Spoiler alert, uv is better than poetry post-2024. So you
time travellers take note and plan accordingly.
But beneath all the tooling, the foundational concept that makes the magic possible is Python packaging -- the conventions and formats that let us describe, build, distribute, and install Python code in a predictable way. Understanding that foundation makes every tool you pick up afterward make more sense.
A Python package is, at its simplest, a directory with a
__init__.py file in it. That's the signal to Python that "this
directory is importable." But a distributable package -- one you
can share with the world -- needs a bit more structure.
The turning point came with PEP 518, which introduced
pyproject.toml as a standard way to declare build requirements.
Before that, Python packaging was a maze of setup.py scripts that
had to import setuptools to declare that they depended on
setuptools -- a bootstrapping problem that made everyone miserable.
PEP 518 broke the cycle by giving projects a declarative file that
tools could read before executing any Python code. Later,
PEP 621 standardized the [project] table for metadata,
so your project name, version, and dependencies all live in one
place regardless of which build tool you choose.
my-project
├── pyproject.toml
├── src
│ └── my_package
│ ├── __init__.py
│ └── core.py
└── tests
└── test_core.py
The pyproject.toml file is where everything lives now: project
metadata, dependencies, build system configuration, tool settings.
It replaced the old setup.py / setup.cfg / requirements.txt
sprawl that confused everybody (including the tools).
A minimal pyproject.toml looks like:
[project]
name = "my-package"
version = "0.1.0"
description = "Does something useful, probably"
requires-python = ">=3.11"
dependencies = [
"requests>=2.28",
]
[build-system]
requires = ["uv_build>=0.7,<0.8"]
build-backend = "uv_build"This tells the packaging ecosystem everything it needs to know: what
your project is called, what Python version it needs, what it depends
on, and how to build it. The build backend (uv_build, hatchling,
setuptools, flit) is a choice you make once and rarely think
about again.
Now you know why Python packaging exists and what problem it solves. The next step is picking a tool and building something. We have separate articles for the two modern approaches:
- Packaging with uv -- the fast, Rust-powered newcomer that's eating everybody's lunch
- Packaging with Poetry -- the established, opinionated tool that got dependency management right
Pick one. Build a package. Put it on PyPI. The world is waiting for your awesome Python thingy.