Skip to main content

plumb

A Symfony-style service container for Python.

Services are declared in YAML files that tie them to the application's configuration through parameters. The builder compiles the declarations and reports every problem at once (missing class, unknown parameter, dangling reference, impossible autowiring, circular reference) before anything runs.

from quazardous.plumb import ContainerBuilder

container = ContainerBuilder(parameters=config, env="prod").load("config/services.yaml").compile()
orders = container.get("app.orders", account="fr")

One dependency (PyYAML), no framework required. Python 3.10+. MIT.

Install

pip install plumb-q        # or: uv add plumb-q

The package is plumb-q on PyPI; the module is quazardous.plumb.

Why

Python dependency-injection libraries declare services in code (decorators, container classes). plumb takes Symfony's approach: the services file is the wiring, and it reads the configuration. Code stays free of lookups and of settings.X reads; swapping an implementation per environment or per configuration value is a line in a file, checked at startup.

A services file

imports:
  - providers.yaml                  # relative to this file; later entries win

parameters:
  api.url: 'https://%host%/api'     # %name% → a declared parameter, or one given to the builder
  api.token: '%env(API_TOKEN)%'     # from the environment…
  env(API_TOKEN): ''                # …with this default when it is not set

services:
  _defaults: { autowire: true, shared: true }

  app.transport:
    class: app.transport.HttpTransport
    arguments: { base_url: '%api.url%', token: '%api.token%' }

  app.transport.Transport: '@app.transport'      # alias: an interface → its implementation

  app.orders.v1:
    class: app.orders.Orders
    shared: false
    runtime: [account]                            # given to get(): get("app.orders.v1", account="fr")
    tags: [{ name: orders_source, version: v1 }]

  app.registry:
    class: app.normalizers.Registry
    arguments: [!tagged_iterator normalizer]      # every service tagged normalizer, in order

  app.idp:
    factory: app.identity.provider                # a dotted callable…
    arguments: ['%idp.driver%', '%idp.url%']
  app.client:
    factory: ['@app.client_factory', create]      # …or a method of another service

when@test:
  services:
    app.transport: { class: app.testing.FakeTransport }

Values

In a value Means
'%name%' the parameter, with its type (a whole-string reference)
'https://%host%/x' the parameter interpolated into the string
'%env(NAME)%' the environment variable; the parameter env(NAME) is its default
'@id' the service id (or what the alias id points to)
'@?id' the service, or None when it is not declared
!tagged_iterator tag the list of the services carrying tag
!tagged_locator {tag: t, index_by: key} a lazy registry (ServiceLocator) of the services carrying t, keyed by the tag attribute key (by id with !tagged_locator t); nothing is built until asked
!service_closure '@id' a callable building or returning the service when called: make(account="fr") is get("id", account="fr")
'%%', '@@' a literal %, a literal @

A service

Key
class dotted path of the class; when there is no class nor factory, the id is the class
factory a dotted callable, or ['@service', 'method']
arguments a list (positional) or a mapping (by name)
autowire fill the remaining constructor arguments by type hint: the service whose id is the type's dotted name, an alias of that name, or the only service of that type
shared one instance for the container (default), or one per get()
runtime arguments given to get() (a value known only when the service is used, such as an account); such a service is built at each get()
tags [name] or [{name: …, attr: …}]
alias this id is another service ('@other' for short)

The container

container.get("id", **runtime)                     # build or reuse
container.param("api.url")
container.tagged("orders_source", version="v1")    # ids, filtered by tag attributes
container.tagged_items("orders_source")            # [(id, attributes)]
container.target("app.orders.v1")                  # its class (or factory), without building it
container.locator("job", index_by="key")           # what !tagged_locator injects
with container.override("app.transport", fake):    # tests
    ...

ContainerBuilder.set(id, obj) registers an object built elsewhere (a synthetic service).

A typical pattern is choosing an implementation from configuration with tags:

(sid,) = container.tagged("orders_source", version=container.param("orders.api"))
source = container.get(sid, account="fr")

A registry of handlers, and a way to build a service later, are injected rather than fetched from the container:

app.scheduler:
  class: app.jobs.Scheduler
  arguments:
    jobs: !tagged_locator { tag: job, index_by: key }       # jobs["orders.import"], jobs.attributes(…)
    make_orders: !service_closure '@app.orders.v1'          # make_orders(account="fr")

Both are lazy, so they also break a circular reference between two services. A missing service, a tag without the index_by attribute and a key given twice are compile errors.

Compile errors

quazardous.plumb.errors.CompileError: container does not compile:
  parameter 'p': %missing% is neither declared nor given
  a: ModuleNotFoundError: No module named 'app.nope'  [config/services.yaml]
  b: ServiceNotFound: no service 'ghost'  [config/services.yaml]
  c: ServiceNotFound: cannot autowire argument 'clock' (Clock): declare it, or register a service of that type
  circular reference: x → y → x

Call compile() where the application starts (for example in a Django AppConfig.ready(), and in a check command): a bad file stops the start, not a request at 3 a.m.

Coming from Symfony

Symfony plumb
services.yaml, imports, parameters, when@env same
%param%, %env(VAR)%, env(VAR) default parameter same
@service, @?service, !tagged_iterator same
!tagged_locator {tag, index_by}, !service_closure same; a closure passes on its runtime: arguments, and the locator also gives tag attributes and classes without building
_defaults: autowire, alias of an interface same; types are Python type hints
factory: [@service, method] same; also a dotted callable
shared: false same
— runtime: arguments given to get()
compiled container compile() checks everything; services are still built lazily
bind, calls, decorates, public, expression language not (yet)

Development

uv run --with pytest --with pyyaml pytest
uv run --with ruff ruff check
cd src && uv run --with mypy --with types-pyyaml python -m mypy --python-version 3.10 \
    --ignore-missing-imports --explicit-package-bases --namespace-packages quazardous/plumb

Metadata

Release files for plumb-q 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for plumb-q 0.3.0
File Size Uploaded Provenance
plumb_q-0.3.0.tar.gz 15.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for plumb-q 0.3.0
File Interpreter ABI Platform
plumb_q-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 30.5 kB

Release files / plumb_q-0.3.0.tar.gz

Download URL plumb_q-0.3.0.tar.gz
Size 15.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d127ad388f05fd4591436e479a4384f1b316b7c19f3c2fe8df3febcbbf790c58
BLAKE2b-256 checksum
How to use checksums
4ec98028d3d0620828f5fa2f208c3e832f0f62dd1ea618b41f2452eddd7613b8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / plumb_q-0.3.0-py3-none-any.whl

Download URL plumb_q-0.3.0-py3-none-any.whl
Size 14.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0be8cafae0dc5f4e500f6fc9e3ada49baf821d64083d8648f601009dbe597b0
BLAKE2b-256 checksum
How to use checksums
4885e1e58c32d31e8a65eff07eb0a0095d6c8c01cce27a41975a11f4d4e48b21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for plumb-q 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for plumb_q-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page