pico-ioc is a lightweight, async-ready, decorator-driven IoC container built for clarity, testability, and performance. It brings Inversion of Control and dependency injection to Python in a deterministic, modern, and framework-agnostic way.
Requires Python 3.11+ (tested on 3.11, 3.12, 3.13 and 3.14)
The pico ecosystem is built for the AI era: machine-readable conventions in every repo, installable AI coding skills, and scaffolds that generate AI-maintainable projects from the first commit.
- Single Purpose – Do one thing: dependency management.
- Declarative – Use simple decorators (
@component,@factory,@provides,@configured) instead of complex config files. - Deterministic – No hidden scanning or side-effects; everything flows from an explicit
init(). - Async-Native – Fully supports async providers, async lifecycle hooks (
__ainit__), and async interceptors. - Fail-Fast – Detects missing bindings and circular dependencies at bootstrap (
init()). - Testable by Design – Use
overridesandprofilesto swap components instantly. - Zero Core Dependencies – Built entirely on the Python standard library. Optional features may require external packages (see Installation).
As Python systems evolve, wiring dependencies by hand becomes fragile and unmaintainable. pico-ioc eliminates that friction by letting you declare how components relate — not how they’re created.
| Feature | Manual Wiring | With pico-ioc |
|---|---|---|
| Object creation | svc = Service(Repo(Config())) |
svc = container.get(Service) |
| Replacing deps | Monkey-patch | overrides={Repo: FakeRepo()} |
| Coupling | Tight | Loose |
| Testing | Painful | Instant |
| Async support | Manual | Built-in (aget, __ainit__) |
- Typed resolution (v2.4):
container.get(UserService)is inferred asUserService, notAny— IDE autocomplete and type-checkers resolve the component. - Public introspection (v2.4):
container.keys()andcontainer.metadata_for(key)enumerate the registry without reaching into the container internals. - Unified Configuration: Use
@configuredto bind both flat (ENV-like) and tree (YAML/JSON) sources via theconfiguration(...)builder (ADR-0010). - Hot config refresh:
container.refresh_config()re-reads tree sources and publishes aConfigChangedevent with the changed prefixes. - Extensible Scanning: Use
CustomScannerto hook into the discovery phase and register functions or custom decorators (ADR-0011). - Async-aware AOP: Method interceptors via
@intercepted_by. - Scoped resolution: singleton, prototype, request, session, transaction, and custom scopes.
- Tree-based configuration: Advanced mapping with reusable adapters (
Annotated[Union[...], Discriminator(...)]). - Observable context: Built-in stats, health checks (
@health), observer hooks (ContainerObserver), and dependency graph export.
pip install pico-iocOptional extras:
-
YAML configuration support (requires PyYAML)
pip install pico-ioc[yaml]
-
Dependency graph export as DOT/SVG (requires Graphviz)
pip install pico-ioc[graphviz]
Breaking Behavior in Scope Management (v2.1.3+): Scope LRU Eviction has been removed to guarantee data integrity.
- Frameworks (pico-fastapi): Handled automatically.
- Manual usage (recommended): open the scope with
with container.scope("scope_name", scope_id, cleanup=True):— on block exit the cached instances are evicted and their@cleanuphooks run automatically. (Added in v2.2.6.) - Manual usage (split lifecycle): when activate and deactivate happen in separate calls (ASGI middleware and the like), call
container.cleanup_scope("scope_name", scope_id)yourself when the context ends to prevent memory leaks. (Public since v2.4.1.)
import os
from dataclasses import dataclass
from pico_ioc import component, configured, configuration, init, EnvSource
# 1. Define configuration with @configured
@configured(prefix="APP_", mapping="auto") # Auto-detects flat mapping
@dataclass
class Config:
db_url: str = "sqlite:///demo.db"
# 2. Define components
@component
class Repo:
def __init__(self, cfg: Config): # Inject config
self.cfg = cfg
def fetch(self):
return f"fetching from {self.cfg.db_url}"
@component
class Service:
def __init__(self, repo: Repo): # Inject Repo
self.repo = repo
def run(self):
return self.repo.fetch()
# --- Example Setup ---
os.environ['APP_DB_URL'] = 'postgresql://user:pass@host/db'
# 3. Build configuration context
config_ctx = configuration(
EnvSource(prefix="") # Read APP_DB_URL from environment
)
# 4. Initialize container
container = init(modules=[__name__], config=config_ctx) # Pass context via 'config'
# 5. Get and use the service
svc = container.get(Service)
print(svc.run())
# --- Cleanup ---
del os.environ['APP_DB_URL']Output:
fetching from postgresql://user:pass@host/db
class FakeRepo:
def fetch(self): return "fake-data"
# Build configuration context (might be empty or specific for test)
test_config_ctx = configuration()
# Use overrides during init
container = init(
modules=[__name__],
config=test_config_ctx,
overrides={Repo: FakeRepo()} # Replace Repo with FakeRepo
)
svc = container.get(Service)
assert svc.run() == "fake-data"Use profiles to enable/disable components or configuration branches conditionally.
# Enable "test" profile when bootstrapping the container
container = init(
modules=[__name__],
profiles=["test"]
)Profiles are typically referenced in decorators or configuration mappings to include/exclude components and bindings.
pico-ioc supports async lifecycle and resolution.
import asyncio
from pico_ioc import component, init
@component
class AsyncRepo:
async def __ainit__(self):
# e.g., open async connections
self.ready = True
async def fetch(self):
return "async-data"
async def main():
container = init(modules=[__name__])
repo = await container.aget(AsyncRepo) # Async resolution
print(await repo.fetch())
# Graceful async shutdown (calls @cleanup async methods)
await container.ashutdown()
asyncio.run(main())__ainit__runs after construction if defined.- Use
container.aget(Type)to resolve components that require async initialization. - Use
await container.ashutdown()to close resources cleanly.
import time
from pico_ioc import component, init, intercepted_by, MethodInterceptor, MethodCtx
# Define an interceptor component
@component
class LogInterceptor(MethodInterceptor):
def invoke(self, ctx: MethodCtx, call_next):
print(f" calling {ctx.cls.__name__}.{ctx.name}")
start = time.perf_counter()
try:
res = call_next(ctx)
duration = (time.perf_counter() - start) * 1000
print(f"← {ctx.cls.__name__}.{ctx.name} done ({duration:.2f}ms)")
return res
except Exception as e:
duration = (time.perf_counter() - start) * 1000
print(f"← {ctx.cls.__name__}.{ctx.name} failed ({duration:.2f}ms): {e}")
raise
@component
class Demo:
@intercepted_by(LogInterceptor) # Apply the interceptor
def work(self):
print(" Working...")
time.sleep(0.01)
return "ok"
# Initialize container (must scan module containing interceptor too)
c = init(modules=[__name__])
result = c.get(Demo).work()
print(f"Result: {result}")-
Export a dependency graph in DOT format:
c = init(modules=[...]) c.export_graph("dependencies.dot") # Writes directly to file
-
Health checks:
- Annotate health probes inside components with
@healthfor container-level reporting. - The container exposes health information that can be queried in observability tooling.
- Annotate health probes inside components with
-
Container cleanup:
- For sync apps:
container.shutdown() - For async apps:
await container.ashutdown()
- For sync apps:
Use cleanup in application shutdown hooks to release resources deterministically.
The full documentation is available within the docs/ directory of the project repository. Start with docs/README.md for navigation.
- Getting Started:
docs/getting-started.md - User Guide:
docs/user-guide/README.md - Advanced Features:
docs/advanced-features/README.md - Observability:
docs/observability/README.md - Cookbook (Patterns):
docs/cookbook/README.md - Architecture:
docs/architecture/README.md - API Reference:
docs/api-reference/README.md - ADR Index:
docs/adr/README.md
pip install tox
toxSee CHANGELOG.md — Significant redesigns and features in v2.0+.
Latest: v2.3.2 (2026-07-10) — fixes lazy components with async @configure resolved via aget(), and defers lazy materialization to first use singleton identity when resolving by base class or component name (#20): the cache was written under the requested key instead of the canonical one, so cold-cache resolutions could create a second singleton.
pico-ioc is designed for a workflow where humans and coding agents build software together. Architecture, conventions and integration patterns are explicit enough that an agent can extend an application without introducing a parallel, incompatible style — and can verify its own changes before proposing them.
The verification loop comes first:
- pico-testing gives any agent (or human) a three-line feedback loop: containers are isolated from the environment by default, the module under test is declared once, and
make_container/make_clientboot exactly what the test names. A change is not done until this loop is green. - pico-initializer scaffolds runnable projects with the canonical layout, so every project starts on the same conventions instead of inventing them.
- pico-examples are reference applications with hermetic test suites plus real-infrastructure smoke tests (Docker Compose, Kubernetes) - each failure path shown is asserted by a test.
- pico-learn turns the patterns into executable lessons; every lab runs green in CI against the pinned published wheels.
- pico-skills gives coding agents task-specific instructions (
/add-component,/add-tests, controllers, repositories, integrations).
Every package in the ecosystem ships the artifacts an agent needs to stay on-architecture: AGENTS.md with the working conventions, llms.txt indexing the docs for machine consumption, architecture decisions recorded in docs/, and documented behaviour pinned by regression tests - with coverage tracked per module on Codecov and 0.0% duplication across the fleet on SonarCloud.
Releases are gated the same way: nothing is published without the full ecosystem booting together and exercising a complete application flow against real infrastructure (PostgreSQL, Redis, RabbitMQ, Kafka). Versioning is strict SemVer with per-release compatibility notes in every changelog.
Install the agent skills for Claude Code or OpenAI Codex:
curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash -s -- iocAll skills: curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash - see pico-skills.
MIT — LICENSE