Skip to content
Back to skills

Hex Wiring

ASecurity

Use when adding a binding in `containers.py` — provider classes, binding lifetimes, the `create_container` composition root and its declaration order, and where each settings class is constructed and bound — every one before the process serves or takes work. Bound here to dishka, with other DI libraries mapped under `## Other bindings`. What a settings class declares, defaults and keeps secret is `python-settings`; the class being bound must already exist — `hex-application`, `hex-capability-...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 14, 2026
developmentpythongoshellapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 14, 2026

npx -y skills add wntic/pyhouse --skill hex-wiring --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hex Wiring?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hex Wiring
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/wntic-hex-wiring/badge)](https://www.skillsdirectory.com/skills/wntic-hex-wiring)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: hex-wiring
description: Use when adding a binding in `containers.py` — provider classes, binding lifetimes, the `create_container` composition root and its declaration order, and where each settings class is constructed and bound — every one before the process serves or takes work. Bound here to dishka, with other DI libraries mapped under `## Other bindings`. What a settings class declares, defaults and keeps secret is `python-settings`; the class being bound must already exist — `hex-application`, `hex-capability-adapter`; the project substrate is `hex-project-setup` and the toolchain `python-toolchain`, not runtime wiring.
paths: ["**/domain/**", "**/application/**", "**/infrastructure/**", "**/restapi/**"]
---

# Hex — Wiring

Handing objects to whoever needs them, from one composition root. Settings are among those objects:
what a settings class declares is `python-settings`, and this skill says where, in a hexagonal service,
it is constructed and bound — at a composition root and nowhere else (`python-settings` rule 13). Each
settings class is shown beside the adapter that reads it: the relational store's `PostgresSettings`
in `hex-persistence`, `FooClassifierSettings` in `hex-capability-adapter`, `RedisSettings` in
`hex-store-repository`, `JwtSettings` in `hex-restapi-auth`. The composition roots of that rule are,
in this catalogue's layout, `src/myapp/containers.py` (the process's container), `migrations/env.py`
(the migration environment, `hex-project-setup`) and `TestInfrastructureProvider` with its fixtures in
`tests/integration/conftest.py` (the test infrastructure, `hex-test-integration-setup`), which
constructs settings with explicit values (`test-principles`).

## When to use vs. neighbours

- What a settings class declares — its namespace, defaults, secrets, derived values, validation → `python-settings`.
- Binding a new class in the composition root, or choosing its lifetime → this skill.
- The class being wired must already exist — a handler, repository, service, adapter → `hex-application`, `hex-persistence`, `hex-store-repository`, `hex-domain-service`, `hex-capability-adapter`.
- A frozen domain-shaped view of settings values the domain consults → the tunable variant in `hex-domain-model`; its provider reads the single field off a settings class and passes it.
- Attaching the composition root to the HTTP app, running its settings check at startup and closing it at shutdown, which runs every declared teardown → `hex-restapi-app`.
- Resolving a bound object inside a route → `hex-restapi-endpoint`.
- The token-verifier port, its adapter and the route dependencies that consume the resolved caller → `hex-restapi-auth`.
- Substituting a binding for a test → `hex-test-integration-setup`.
- Which libraries the project carries and the Alembic bootstrap → `hex-project-setup`; the ruff and mypy configuration → `python-toolchain`. Both are laid once, not per binding.
- What a settings class, its env prefix or a provider method is called → `naming`.
- Whether a class may be bound here at all, and which layer it belongs to → `hex-architecture`; it owns the layer contract this composition root sits outside of.
- The unit-of-work factory binding a multi-repository transaction needs → `hex-persistence` (`UNIT_OF_WORK.md`); the compensating-handler shape → `hex-application`.

## Template — dishka

`src/myapp/containers.py` is the only file this skill touches. Bindings are grouped into provider classes
by layer and by subdomain; `create_container` assembles them. **Every dependency is resolved by type** —
no binding is reached by its attribute name, so renaming a class cannot silently break a call site. The
template is the **base** a project extends — the provider classes and the handlers — and binds no
store, no optional adapter and no feature. **A store's or an add-on's binding lives with what it
binds**: the relational engine, session factory and repository in `hex-persistence`, the HTTP gateway
in `hex-capability-adapter`, the Redis repository in `hex-store-repository`, the token verifier in
`hex-restapi-auth`, the tunable in `hex-domain-model`, the unit-of-work factory in `hex-persistence`; a project merges the ones it has.

**Read `CONTAINER.md`** in this skill's directory before writing or extending `containers.py`. It
carries the base composition root — the provider classes in declaration order, `create_container` and
the settings check an entrypoint runs at start — and how an add-on binding merges into it; only `SKILL.md` is loaded automatically.

## Other bindings

- **`dependency-injector`.** The obligations are identical; what changes is the spelling and one extra
  rule. A `containers.DeclarativeContainer` subclass replaces the provider classes, `providers.*`
  replace the factories, and — the rule dishka does not need — **a binding is reached at the call site
  by its attribute name, which must be the snake_case form of the class** — the call site writes
  `<root>.<snake_case_attribute>()`. That name-based contract is unenforced: rename the class and the call
  site breaks at runtime. Test substitution differs in kind, and its trap is
  `hex-test-integration-setup`'s.

  | dishka | dependency-injector |
  |---|---|
  | `Provider` + `@provide` | `DeclarativeContainer` + `providers.*` |
  | `Scope.APP` | `providers.Singleton` |
  | `Scope.REQUEST` | `providers.Factory` |
  | `provides=IFooRepository` | `providers.Provider[IFooRepository]` annotation |
  | constructor arguments resolved by annotation | each argument passed explicitly |
  | generator factory + `container.close()` | `providers.Resource`, or teardown in the entrypoint |
  | `FromDishka[T]` at the call site | `container.<snake_case_attribute>()` |
  | an extra provider with `override=True`, before the container is built | `.override()` / `.reset_override()` on a built container |
  | `resolve_settings` over `SettingsProvider`'s declarations | each provider of the settings sub-container, walked with `.traverse()`, called once at start |

- **Manual composition — a plain factory module.** A service small enough not to want a framework
  writes `create_container()` as a function that constructs each object and returns a frozen dataclass
  of them. Every rule below still holds; declaration order becomes literal statement order, teardown
  becomes an `AsyncExitStack` the entrypoint closes, and per-operation lifetime becomes a stored factory
  callable rather than a stored instance. Building such a root constructs its settings, so it is its
  own startup check, and its construct smoke passes explicit settings rather than reading the
  environment.

## Rules

1. **Settings are constructed only at a composition root and bound there by type** (`python-settings`
   rule 13) — never in a handler, an adapter, an entrypoint module or another settings class. An adapter
   receives the whole settings object it reads, by type, as a constructor parameter; nothing below the
   root reads the environment.
2. **Every settings class the composition root provides is built when the process starts, before it
   serves or takes work** (`python-settings` rule 13). Under a graph that resolves lazily every
   entrypoint takes that step once at start — the REST shell in its lifespan (`hex-restapi-app`), a
   consumer's launcher before its first message — as a step of its own rather than part of building the
   root, which a test does with no environment. A root that several entrypoint processes share makes
   each of them require every settings class it provides, so each is deployed with all of them.
3. **The process's container module is the composition root.** Every concrete class is bound to the
   protocol it satisfies there and **only** there. Domain and application code never instantiates a
   concrete type.
4. **Every binding declares a lifetime, and the choice is deliberate.** Nothing is bound without an
   answer to "how long does this live".
5. **Anything holding a resource that must be released declares its teardown in the same place as its
   construction**, so construction and release cannot drift apart. The entrypoint closes the composition
   root exactly once, which runs every declared teardown (`hex-restapi-app`).
6. **Bindings are reached by type, not by name.** A call site names the type it needs; the composition
   root decides what satisfies it. Nothing outside the composition root may depend on how a binding is spelled.
7. **A unit of work is bound as its factory callable, never as an instance**, so each call opens its own
   transaction and no two callers share one. `hex-persistence`'s `UNIT_OF_WORK.md` shows the form it binds.

### Where a settings class sits

- **The module a settings class sits in** → `hex-conventions` block A. There is no central settings
  module.
- **The prefix's stem names the deployable, never a bounded context.** `naming` owns the prefix
  (`MYAPP_` plus the integration's segment). What only a hexagonal service adds: a deployable serves
  every context in it, so a context-named stem becomes wrong the moment a second context shares the
  store — shared settings collapse to one object (`hex-conventions` block C). Working inside one
  context, that context is the salient name; stem on the deployable anyway.

### Lifetimes

| Lifetime | Use for | Examples |
|---|---|---|
| **Process** | Stateless or expensive-to-construct objects whose lifetime spans the process. | Settings (`*Settings`), the engine, the session factory, a token verifier, a stateless library-backed adapter, **tunable value objects**, a stateless factory callable. |
| **Per operation** | Instances meant to be fresh for each request or each job, cheap to construct. | Every `*Handler`, every `*Repository`, **domain services** that compose them, a stateful adapter bound to per-request state. |

**Default to per-operation for application and domain artifacts. Reserve process lifetime for objects
that own a connection pool, parse the environment once, or are pure-data configuration.**

The pitfall: giving a repository process lifetime looks fine because it is stateless, but it freezes the
session factory it was built with for the life of the process, which defeats substituting one for a test
and forecloses any future per-request session. **Repositories are per-operation.**

### Declaration order

Group bindings in dependency order and keep them in that order:

- **Settings first** — everything else may depend on them.
- **Long-lived infrastructure** — engine, session factory, verifiers.
- **Cross-cutting helpers** — capability adapters needed by several subdomains.
- **Per-subdomain block:** repository → services that use it → handlers that use them.
- **Cross-subdomain dependencies come first.** If subdomain A's repository is consumed by subdomain B's
  handlers, declare it before B's block.

Where the mechanism evaluates the composition root top to bottom, this ordering is **load-bearing** — a
binding may only reference an earlier one. Where the mechanism resolves by type it is the reading order
instead, and a binding that would have to forward-reference is the signal that it is grouped wrong. When
adding a binding, find the right section and insert it after the latest declaration it depends on.

### Naming and access

- Factory and attribute naming → `naming`. These names are internal to the composition root.

### Settings lifecycle in the composition root

- Each `*Settings` is process-lifetime, built with no arguments by a factory in the composition root's
  one settings group. The startup check reads that group, so a settings factory declared anywhere else
  escapes it; the dishka spelling is in `CONTAINER.md`.
- A **tunable value object** that needs a single field gets a factory of its own, which reads the field
  off the settings object and passes it. Never pass raw settings fields around otherwise.

### What never goes in the composition root

- **No business logic.** It only wires.
- **No conditionals on the environment.** Different environments produce different settings *values*; the wiring
  stays the same. Hide a feature flag behind a settings field inside the implementation, never behind a
  branch in the wiring.
- **No imports from `restapi/` or another entrypoint.** The composition root sits below the entrypoint
  layer.
- **No mutable module-level state.** `create_container` returns a new composition root per call; a
  module-level singleton makes the test seam unreachable.
- **No instantiation of concrete domain types** — entities, value objects. It builds services, not data.

## Inlined typing / import rules

- Settings field typing and imports → `python-settings`.

- **A factory's return annotation is the binding's type**, and it is the whole contract:
  `-> PostgresSettings` binds `PostgresSettings`, `-> AsyncIterator[AsyncEngine]` binds `AsyncEngine`
  with a teardown. Use `provides=<Protocol>` when the class-attribute form binds an adapter to its port.
  An unannotated or loosely annotated factory binds the wrong type or fails at container construction,
  not at the call site.

- **Import each class from the package that DIRECTLY re-exports it — one `from .module import *` hop —
  never a grandparent** (`python-packaging`). This bites the nested infrastructure layout: a repository class lives in
  `infrastructure/<store>/repositories/<x>.py`, so import it from the **`repositories` subpackage** —
  `from myapp.infrastructure.postgres.repositories import FooRepository` — **not** from the `<store>`
  technology package. The technology-package form resolves at runtime but mypy reports `[attr-defined]`, because the
  intermediate `repositories/__init__.py` has a computed `__all__` mypy cannot evaluate across the
  `from .repositories import *` hop. A class sitting directly under the technology package — the `engine` or
  `settings` module, a capability adapter — is one hop away, so importing it from the technology package is
  correct.

- No `from __future__ import annotations` (`python-style`).

## Package wiring

Composition-root location → `hex-architecture`.

`containers.py` needs no package wiring at all: it is `src/myapp/containers.py`, a module of the
distribution's root package, and the root `__init__.py` does not re-export it — that file stays empty
(`python-packaging`'s carve-out for an application's root). For the classes it imports, follow
`python-packaging`.

## Hard stops

- Asked for an environment read, a new settings field, or a default on one → stop, use `python-settings`.
- Asked to add a binding whose dependency is not yet declared → stop, that dependency's own skill
  runs first.
- The composition root is asked to bind a store connection or transaction handle per operation, so a
  repository can be injected with one outside a unit of work → stop, nothing would then own the commit;
  use the standalone repository form, which opens and owns its own (`hex-persistence` for a relational
  store, `hex-store-repository` for a client-style one), or a unit of work (`hex-persistence`). A store
  whose client is the connection and has nothing to commit is bound at process lifetime, not per
  operation.

Files in this skill

  • CONTAINER.md4.2 KB
  • SKILL.md14.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.