making python packages the modern way
Although the [uv package and project management tool][uv] has been taking the Python community by storm, I’ve found its documentation to be annoying incomplete. This is the first in a series of notes that I’m writing to address some places of chronic misunderstandings I’ve seen in the community.
The first of these is how to handle imports within the package being written under uv. I’ve not found a place where this is described clearly and adds to the longstanding Python problems of getting relative imports correct.
If you are developing a Python package to be used by others you have to make a Python package and then you can import that directly by name. In practice, I’ve found it worthwhile to start every project as a package because the little extra mechanism added at the start makes later distribution effortless.
The command uv init --package octavius
will create a package structure containing
octavius/
├── pyproject.toml
└── src
└── octavius
└── __init__.py
Then, if you cd octavius
and uv sync
the package octavius
will be installed into
your virtualenv.
You have created a package octavius and you can import it directly in your notebooks like:
import octavius
Now, if I create octavius/toys.py
with a function lego
in it. I could then
from octavius import toys
toys.lego()
or similar.
To create a submodule in octavius/friends
just create the directory and put an empty
__init__.py
to mark it as a module. Then, with octavius/friends/mary.py
you could
import that with
from octavius.friends import mary
print(f"mary.{hobbies()=}")
a big gotcha
If you are missing a build system in your pyproject.toml
you will not be able to
import your package. Unfortunately, uv
gives no indication that anything is wrong, you
will just get an ImportError
when you try to import your package.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
You can confirm that uv
has installed your package with
uv pip list | grep octavius