Skip to main content

A minimalist, zero-dependency Inversion of Control (IoC) container for Python.

Project description

📦 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+


⚖️ 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+)

  • Unified Configuration: Use @configured to bind both flat (ENV-like) and tree (YAML/JSON) sources via the configuration(...) builder (ADR-0010).
  • 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]
    

⚠️ 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: You must explicitly call container._caches.cleanup_scope("scope_name", scope_id) when a context ends to prevent memory leaks.

⚙️ 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+.


🤖 Claude Code Skills

This project includes pre-designed skills for Claude Code, enabling AI-assisted development with pico-ioc patterns.

Skill Command Description
Pico Component Creator /pico-component Creates components with DI, scopes, factories and interceptors
Pico Test Generator /pico-tests Generates tests for pico-framework components

See Skills documentation for full details and installation instructions.


📜 License

MIT — LICENSE

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pico_ioc-2.2.3.tar.gz (233.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pico_ioc-2.2.3-py3-none-any.whl (43.0 kB view details)

Uploaded Python 3

File details

Details for the file pico_ioc-2.2.3.tar.gz.

File metadata

  • Download URL: pico_ioc-2.2.3.tar.gz
  • Upload date:
  • Size: 233.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pico_ioc-2.2.3.tar.gz
Algorithm Hash digest
SHA256 196b2561b1c1dbdbbf70827b49b35c71131948ad73f8f4521009e2eaef1aa062
MD5 d820000438ca2b8d376d26d8ddcbbb61
BLAKE2b-256 54d0b4041a4954d5a9c8cbd9485fda7ef57609e243f21d1ee0b0041f7d9aaf58

See more details on using hashes here.

File details

Details for the file pico_ioc-2.2.3-py3-none-any.whl.

File metadata

  • Download URL: pico_ioc-2.2.3-py3-none-any.whl
  • Upload date:
  • Size: 43.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pico_ioc-2.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 e8b2376da46636237d2d5f9b83c742ec66f6044a4321b37ba3db5addde8a4c0c
MD5 1461ba2b98d32f739b864b54fb88896b
BLAKE2b-256 3eb7b845b7548d9f29be2b8e0caca4847bc9736833c6f63c3b40a9b574a3fb5f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page