catalogue
catalogue is a tiny Python registry for mapping stable string names to functions or other objects. A library creates a namespaced Registry, extensions register implementations with a decorator or direct call, and configuration can store only the chosen name instead of trying to serialize executable code. Optional Python entry-point discovery lets separately installed packages contribute implementations without an application importing each plugin module itself.
Catalogue is a good fit for library authors who need a string-to-callable registry and standard entry-point discovery, nothing more. Application code with a handful of callbacks should use a dict, while complex plugin contracts deserve pluggy or stevedore.
Use it if
- You maintain a Python library that needs a small public plugin surface keyed by serializable names
- Configuration, logs, or saved models must record which callable was selected without pickling the callable
- Third-party distributions should expose implementations through standard Python package entry points
- You want the same simple registry style used in the Explosion ecosystem without adopting a full plugin manager
- You only need callbacks inside one application: passing the callable directly or using a local dict avoids a global registry and import-order behavior
- You need hook specifications, ordered multi-plugin calls, wrappers, or validation: pluggy provides a real hook system while catalogue maps one name to one object
- You need type-safe lookup: Registry.get and get_all return Any, so catalogue does not prove that every registered object has the expected signature
- You need isolated registries with duplicate-name protection: all Registry instances write to one module-level REGISTRY dict, creating the same namespace twice raises, and registering the same full key overwrites its previous value
- Plugins can be installed while a process is running: available entry points are captured once when catalogue is imported, so discovery will not refresh until the module or process is reloaded
Setup reality
pip install catalogue is enough on current Python, with no required configuration file and no runtime dependency for Python 3.8 or later. The published metadata still supports Python 3.6 and 3.7 by conditionally installing zipp and typing-extensions, while the README directs Python 2 users to the old 1.x line. The real setup cost is deciding namespace ownership and import behavior. catalogue.create('your_package', 'loaders') raises RegistryError if that namespace was already created in the process, so define it once in a stable module and import that object everywhere else. Decorator registration only runs when the defining module is imported. For independently distributed plugins, create the registry with entry_points=True and publish an entry-point group formed by joining the namespace with underscores, such as your_package_loaders. Entry-point objects are loaded when looked up and can raise their own import errors. Catalogue snapshots installed entry points at its own import time, so adding a distribution to a live environment does not update an already running process. Direct registration accepts any object and duplicate names overwrite silently; the package performs no signature, protocol, or return-value validation. Tests that register global entries can contaminate later tests in the same interpreter, and the only removal helper is private, so use unique test namespaces, process isolation, or carefully restore catalogue.REGISTRY in fixtures. There are no credentials, native builds, or services, but there is also no lifecycle management around plugins.
Patterns
Create one namespaced registrycreate-registry
import catalogue
loaders = catalogue.create('acme_data', 'loaders')Create a namespace once in a stable module. A second create call for the same namespace raises RegistryError even if no entries have been registered yet.
Register a callable with a decoratorregister-decorator
@loaders.register('json')
def load_json(path):
import json
with open(path, encoding='utf8') as file_:
return json.load(file_)Registration occurs when Python imports this module. If nothing imports it, the decorator never runs and the name will be absent.
Register an existing callable directlyregister-directly
def load_text(path):
return path.read_text(encoding='utf8')
loaders.register('text', func=load_text)The func argument is keyword-only. Registering the same name again silently replaces the previous object in the global registry.
Use the Registry object as the decoratoruse-registry-call
@loaders('bytes')
def load_bytes(path):
return path.read_bytes()Registry.__call__ delegates to register. This is shorter but can be less obvious to readers unfamiliar with catalogue.
Resolve a configured implementationresolve-and-call
def load(path, loader_name: str):
loader = loaders.get(loader_name)
return loader(path)An unknown name raises catalogue.RegistryError and the message lists available names. The return type is Any, so your code owns signature validation.
List direct and entry-point registrationslist-registrations
for name, loader in loaders.get_all().items():
print(name, loader)With entry_points=True this loads every matching entry point. Import failures inside any plugin can therefore make get_all fail.
Check for a name before lookupcheck-registration
if 'json' in loaders:
value = loaders.get('json')(path)
else:
value = fallback(path)For entry-point registries, containment may load the matching plugin rather than performing a side-effect-free metadata check.
Accept plugins from installed distributionsenable-entry-points
import catalogue
renderers = catalogue.create(
'acme_reports',
'renderers',
entry_points=True,
)The entry-point group becomes acme_reports_renderers. Available entry points are captured when catalogue itself is imported.
Advertise a plugin in pyproject.tomlpublish-entry-point
[project.entry-points.acme_reports_renderers]
pdf = 'acme_pdf:render_pdf'The group must exactly match the catalogue namespace joined by underscores. The target module and object are imported when catalogue loads that entry point.
Load one optional plugin with a fallbackload-one-entry-point
renderer = renderers.get_entry_point('pdf', default=None)
if renderer is None:
renderer = render_textget_entry_point returns the default only when the name is absent. An installed plugin that fails during import still raises its import exception.
Find where a registered callable came frominspect-registration
info = loaders.find('json')
print(info['module'])
print(info['file'], info['line_no'])
print(info['docstring'])Cython and other non-inspectable callables can produce None for file and line number. find first performs a normal lookup and can load an entry point.
Give each test registry a unique namespaceavoid-test-collisions
from uuid import uuid4
import catalogue
registry = catalogue.create('tests', uuid4().hex)
registry.register('fake', func=lambda value: value)Catalogue keeps process-wide global state and exposes no public unregister method. Unique namespaces avoid duplicate-create errors in a shared test interpreter.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pluggy | PyPI | Use it for specified hooks, multiple implementations, wrappers, ordering, and validation across a real plugin system |
| stevedore | PyPI | Use it when entry-point discovery, loading strategies, drivers, and extension managers are the main job |
| dependency-injector | PyPI | Use it when named implementations are part of a larger dependency graph with scopes, providers, and configuration |