catalogue review
catalogue 2.0.10 is a small Python registry that turns a string such as `json` into a function or other object kept in process memory. A package can expose named extension points, let another module register implementations with a decorator, and store the chosen name in config instead of trying to serialize a callable. It can also resolve plugins published through Python entry points. Our install occupied 1 MB and imported in 0.21 seconds, but the package has no `py.typed` marker and does not check the signature of anything registered. The current release only updates packaging and tests for Python 3.12; it does not add a new registry operation.
catalogue 2.0.10 installed in 0.2 seconds and used 1 MB in our sandbox, making it a cheap fit for libraries that need one serializable name per extension. Skip it when you need typed hook contracts, several handlers per event, or plugins added without restarting the process.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import catalogue in 0.21s · pure Python · requires Python >=3.6 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does catalogue install cleanly?
Yes. In a fresh container with an empty cache, pip install catalogue finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does catalogue need to run?
Python >=3.6, and nothing compiled: it is pure Python. In our run import catalogue succeeded in 0.21s.
catalogue or pluggy: which should you use?
pluggy: Use it when a declared hook may have several implementations and ordering or wrappers matter. catalogue 2.0.10 installed in 0.2 seconds and used 1 MB in our sandbox, making it a cheap fit for libraries that need one serializable name per extension.
When should you not use catalogue?
The callbacks live inside one application and never need serializable names. A normal dict or an injected callable avoids global state and import-order behavior.
Use it if
- A Python library needs callable names that remain readable in JSON, TOML, logs, or saved pipeline metadata.
- Optional wheels should add implementations through a packaging entry-point group without requiring users to import each plugin module.
- Your extension model is exactly one name resolving to one object, and your own package can validate that object's callable contract.
- Registration can happen during process startup, before workers begin serving requests or loading saved configuration.
- The callbacks live inside one application and never need serializable names. A normal dict or an injected callable avoids global state and import-order behavior.
- One event must run several handlers with ordering, wrappers, or declared hook signatures. pluggy implements that model; catalogue resolves one object for each full name.
- Static type checking must prove the registered callable signature. The wheel has no `py.typed` marker, and `Registry.get()` returns `Any`.
- Tests need a supported reset or unregister call. Every Registry writes into the module-level `REGISTRY`, while removal exists only as the private `_remove` function.
- A long-running process must notice a plugin installed after startup. catalogue captures available entry-point metadata once when its module imports.
- Accidental duplicate names must raise an error. Registering the same full name again silently replaces the previous object.
Setup reality
Our fresh Python 3.12 install of catalogue 2.0.10 finished in 0.2 seconds. It left 1 package and 1 MB on disk, with 2 direct dependencies and no native build step. import catalogue worked in 0.21 seconds, and pip-audit reported 0 known vulnerabilities. The package requires Python 3.6 or newer and is pure Python. Its wheel does not contain py.typed, so treat values returned by the registry as untyped at package boundaries.
No service, credential, environment variable, or config file is required. The setup choice is where the namespace is created. Define catalogue.create('acme', 'loaders') once in a stable module, then import that Registry everywhere else. A second create call for the same namespace raises RegistryError, even if it comes from another test or import path in the same interpreter.
Decorator registration runs only when Python executes the module containing the decorator. For separately installed plugins, create the Registry with entry_points=True and publish a group whose name is the namespace parts joined with underscores. catalogue 2.0.10 loads the selected entry point during lookup. An import error inside that plugin therefore appears when get, get_all, or even a membership check tries to load it.
Entry-point metadata is snapshotted once at import catalogue, so installing a new plugin into a live process does not refresh discovery. Registered objects also remain in the module-level mapping for the interpreter's lifetime. Reusing a full name overwrites its value. Give tests unique namespaces or restore the mapping in a fixture, and validate callable arguments yourself before invoking anything returned as Any.
Patterns
Create one shared namespace create-registry
# acme/loaders.py
import catalogue
loaders = catalogue.create('acme', 'loaders')Version 2.0.10 raises catalogue.RegistryError if the same namespace has already been created in this interpreter.
Register a function when its module imports register-decorator
from acme.loaders import loaders
@loaders.register('json')
def read_json(path):
import json
with open(path, encoding='utf8') as stream:
return json.load(stream)The name appears only after Python executes this module, so startup code must import it before calling `get('json')`.
Register an existing object register-function
from acme.loaders import loaders
def read_bytes(path):
return path.read_bytes()
loaders.register('bytes', func=read_bytes)`func` is keyword-only. Registering `bytes` again replaces the existing object instead of raising an error.
Use the Registry itself as a decorator callable-registry
from acme.loaders import loaders
@loaders('text')
def read_text(path):
return path.read_text(encoding='utf8')Calling a Registry forwards to `register`, so it has the same import-time side effect and duplicate-name behavior.
Resolve a loader named in configuration resolve-name
from acme.loaders import loaders
def load_file(path, config):
loader = loaders.get(config['loader'])
return loader(path)An unknown name raises RegistryError and includes the names currently visible under this namespace.
List every object in a namespace list-registrations
from acme.loaders import loaders
for name, loader in loaders.get_all().items():
print(f'{name}: {loader!r}')With entry points enabled, `get_all()` loads every matching plugin; one broken plugin import can stop the whole call.
Check for a loader before resolving it check-name
from acme.loaders import loaders
loader = loaders.get('json') if 'json' in loaders else read_text
result = loader(path)For an entry-point Registry, the membership check can load plugin code instead of performing a side-effect-free dictionary test.
Discover extensions from installed wheels enable-entry-points
import catalogue
renderers = catalogue.create(
'acme_reports',
'renderers',
entry_points=True,
)This Registry reads the `acme_reports_renderers` entry-point group that catalogue captured when the process imported it.
Publish an extension in pyproject.toml publish-entry-point
[project.entry-points.acme_reports_renderers]
pdf = 'acme_pdf:render_pdf'The group must match the Registry namespace joined with underscores, and the value must identify an importable module object.
Fall back when one entry point is absent optional-plugin
renderer = renderers.get_entry_point('pdf', default=None)
if renderer is None:
renderer = render_text
output = renderer(report)The default handles a missing name. If an installed `pdf` plugin raises while importing, catalogue propagates that exception.
Find where a registered function came from inspect-registration
details = loaders.find('json')
print(details['module'])
print(details['file'], details['line_no'])
print(details['docstring'])For Cython and other non-inspectable objects, version 2.0.10 can return `None` for the file and line number.
Give each test a fresh namespace isolate-tests
from uuid import uuid4
import catalogue
registry = catalogue.create('tests', uuid4().hex)
registry.register('fake', func=lambda value: value)There is no public unregister API. A unique namespace avoids collisions while the module-level registry survives for the test process.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pluggy | PyPI | Use it when a declared hook may have several implementations and ordering or wrappers matter. |
| stevedore | PyPI | Use it when loading named drivers and managing enabled extensions are the center of the plugin system. |
| importlib-metadata | PyPI | Use it on older Python versions when direct entry-point enumeration is enough and you do not need a registry wrapper. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

