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
'%%', '@@' 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
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")

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
_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, service locators, 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.2.1

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.2.1
File Size Uploaded Provenance
plumb_q-0.2.1.tar.gz 13.2 kB With provenance Details

Built distribution (wheel)

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

Total release size: 26.0 kB

Release files / plumb_q-0.2.1.tar.gz

Download URL plumb_q-0.2.1.tar.gz
Size 13.2 kB
Tags Source
SHA-256 checksum
How to use checksums
7c3d5082a7a78a75be948b5bf37747faff7e91d519f695fb93192bedbf11b37a
BLAKE2b-256 checksum
How to use checksums
6512f64cbc844bccd931f4d8a6340ba7936f8528fb93fe34fb3d63e05fc4b238
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.2.1-py3-none-any.whl

Download URL plumb_q-0.2.1-py3-none-any.whl
Size 12.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c90109d61272d662f062a16a13fdd38def508f43691f73b6b85ce30e29d1a3fc
BLAKE2b-256 checksum
How to use checksums
aa174664d05df58155b20a649dbe8706a312ccee869a1a68c87a9f493fe86a51
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.2.1

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.2.1.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