The libspec.* Namespace Extension Model
libspec is designed to be extended. Third-party libraries can contribute
new modules under the libspec.* namespace — so users can write:
from libspec import Feature
from libspec.diataxis import Diataxis
from libspec.conventional_commits import Commit
class MyBaseFeature(Feature, Diataxis, Commit): pass
Each of those imports comes from a different, independently-published package, but they all feel like part of one coherent library. This page explains how that works and why it was designed this way.
The problem: regular packages are closed
When Python imports libspec, it records one location for the package:
Any subsequent from libspec.X import Y looks only in that one directory.
A separate installed package that ships a libspec/X.py file will simply never
be found, even if it is on sys.path.
This is fine for single-publisher packages, but it makes ecosystem composability
impossible — you would have to fork libspec or vendor everything into a
monorepo to achieve the same import experience.
The solution: pkgutil.extend_path
libspec opts into namespace extension with a single line at the top of its
__init__.py:
pkgutil.extend_path walks every entry on sys.path, looks for a
sub-directory (or .pth file) named libspec, and appends any it finds to
libspec.__path__. The result is that libspec.__path__ becomes a list of
every libspec/ directory across all installed packages:
libspec.__path__ = [
'.../site-packages/libspec', # libspec itself
'.../libspec-diataxis/src/libspec', # libspec-diataxis (editable)
'.../site-packages/libspec', # libspec-conventional-commits, etc.
]
Python's import machinery then searches all of them in order, so
libspec.diataxis and libspec.conventional_commits are discovered naturally.
What a sibling library looks like
A sibling library needs nothing beyond a single source file and a standard
pyproject.toml. No hooks, no .pth tricks, no modifications to libspec:
# pyproject.toml
[project]
name = "libspec-diataxis"
dependencies = ["libspec>=10.5.2"]
[tool.hatch.build.targets.wheel]
packages = ["src/libspec"]
When installed (including in editable mode via uv add), the package's
src/ directory lands on sys.path. pkgutil.extend_path then finds
src/libspec/ and adds it to libspec.__path__ automatically.
Why a module file, not a package directory?
libspec.diataxis is a plain .py file, not a diataxis/ directory with an
__init__.py. This is intentional:
- A module file needs no
__init__.py— it is the module. - A namespace package directory (
diataxis/without__init__.py) would be an empty container with no attributes —from libspec.diataxis import Diataxiswould fail. - A regular package directory (
diataxis/with__init__.py) works, but adds a layer of indirection for no benefit when the entire public surface is a single class.
The simplest shape that makes from libspec.diataxis import Diataxis work is
a file called diataxis.py. That is what sibling libraries should ship.
The contract for sibling library authors
To publish a libspec.* sibling library:
- Declare
libspec>=10.5.2as a dependency (the minimum version that includesextend_path). - Ship your module at
src/libspec/<name>.py. - Set
packages = ["src/libspec"]inpyproject.toml. - Done — no hooks, no coordination with the
libspecmaintainers required.
See How to build a libspec.* extension library
for a step-by-step guide.