Skip to main content

pico-ioc: A Robust, Async-Native IoC Container for Python

PyPI Ask DeepWiki License: MIT CI (tox matrix) codecov Quality Gate Status Duplicated Lines (%) Maintainability Rating PyPI Downloads Docs Interactive Lab

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.


Core Principles

  • 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 overrides and profiles to swap components instantly.
  • Zero Core Dependencies – Built entirely on the Python standard library. Optional features may require external packages (see Installation).

Why pico-ioc?

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__)

Highlights (v2.2+)

  • Typed resolution (v2.4): container.get(UserService) is inferred as UserService, not Any — IDE autocomplete and type-checkers resolve the component.
  • Public introspection (v2.4): container.keys() and container.metadata_for(key) enumerate the registry without reaching into the container internals.
  • Unified Configuration: Use @configured to bind both flat (ENV-like) and tree (YAML/JSON) sources via the configuration(...) builder (ADR-0010).
  • Hot config refresh: container.refresh_config() re-reads tree sources and publishes a ConfigChanged event with the changed prefixes.
  • Extensible Scanning: Use CustomScanner to 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.

Installation

pip install pico-ioc

Optional extras:

  • YAML configuration support (requires PyYAML)

    pip install pico-ioc[yaml]
    
  • Dependency graph export as DOT/SVG (requires Graphviz)

    pip install pico-ioc[graphviz]
    

Important Note

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 @cleanup hooks 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.)

Quick Example (Unified Configuration)

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

Testing with Overrides

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"

Profiles

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.


Async Components

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.

Lifecycle & AOP

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}")

Observability & Cleanup

  • 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 @health for container-level reporting.
    • The container exposes health information that can be queried in observability tooling.
  • Container cleanup:

    • For sync apps: container.shutdown()
    • For async apps: await container.ashutdown()

Use cleanup in application shutdown hooks to release resources deterministically.


Documentation

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

Development

pip install tox
tox

Changelog

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


Built for AI-assisted development

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_client boot 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 -- ioc

All skills: curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash - see pico-skills.


License

MIT — LICENSE

Metadata

Release files for pico-ioc 2.5.2

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

Source distribution (sdist)

Source distribution for pico-ioc 2.5.2
File Size Uploaded
pico_ioc-2.5.2.tar.gz 315.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pico-ioc 2.5.2
File Interpreter ABI Platform
pico_ioc-2.5.2-py3-none-any.whl Python 3 none any Details

Total release size: 378.2 kB

Release files / pico_ioc-2.5.2.tar.gz

Download URL pico_ioc-2.5.2.tar.gz
Size 315.3 kB
Tags Source
SHA-256 checksum
How to use checksums
3d99b282e1b8c5c3c7fe357d364d9c4c17b4bddd49cde7f1af595d380119afe0
BLAKE2b-256 checksum
How to use checksums
c61175937ee6d08afb3d21d9b07566ba19602d38bed01a9d258ed68186b43c6a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 5, 2026.

Transparency log

Release files / pico_ioc-2.5.2-py3-none-any.whl

Download URL pico_ioc-2.5.2-py3-none-any.whl
Size 62.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
be5cd91f4966ee8bdc919b0c0ae8c111bca1a4709dbaef8aa19fb3543d65d6f7
BLAKE2b-256 checksum
How to use checksums
ff8eca940cd457e651ca6bc4d36b276c9671258c212e1c8eea01978b503e471e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.5.2 This release

2 release files

2.5.1

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.4

2 release files

2.3.3

2 release files

2.3.2

2 release files

2.3.1

2 release files

2.3.0

2 release files

2.2.7

2 release files

2.2.6

2 release files

2.2.5

2 release files

2.2.4

2 release files

2.2.3

2 release files

2.2.2

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.3

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.5

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

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