# π¦ Pico-IoC: A Minimalist IoC Container for Python
[](https://pypi.org/project/pico-ioc/)
[](https://opensource.org/licenses/MIT)

**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control container for Python.
Build loosely-coupled, testable apps without manual wiring. Inspired by the Spring ecosystem.
---
## β¨ Key Features
* **Zero dependencies** β pure Python.
* **Decorator API** β `@component`, `@factory_component`, `@provides`.
* **Auto discovery** β scans a package and registers components.
* **Eager by default, fail-fast** β non-lazy bindings are instantiated immediately after `init()`. Missing deps fail startup.
* **Opt-in lazy** β set `lazy=True` to defer creation (wrapped in `ComponentProxy`).
* **Factories** β encapsulate complex creation logic.
* **Smart resolution** β by **parameter name**, then **type annotation**, then **MRO fallback**, then **string(name)**.
* **Re-entrancy guard** β prevents `get()` during scanning.
* **Auto-exclude caller** β `init()` skips the calling module to avoid double scanning.
---
## π¦ Installation
```bash
pip install pico-ioc
```
---
## π Quick Start
```python
from pico_ioc import component, init
@component
class AppConfig:
def get_db_url(self):
return "postgresql://user:pass@host/db"
@component
class DatabaseService:
def __init__(self, config: AppConfig):
self._cs = config.get_db_url()
def get_data(self):
return f"Data from {self._cs}"
container = init(__name__) # blueprint runs here (eager + fail-fast)
db = container.get(DatabaseService)
print(db.get_data())
```
---
## π§© Custom Component Keys
```python
from pico_ioc import component, init
@component(name="config") # custom key
class AppConfig:
db_url = "postgresql://user:pass@localhost/db"
@component
class Repository:
def __init__(self, config: "config"): # resolve by NAME
self.url = config.db_url
container = init(__name__)
print(container.get("config").db_url)
```
---
## π Factories and `@provides`
* Default is **eager** (`lazy=False`). Eager bindings are constructed at the end of `init()`.
* Use `lazy=True` for on-first-use creation via `ComponentProxy`.
```python
from pico_ioc import factory_component, provides, init
COUNTER = {"value": 0}
@factory_component
class ServicesFactory:
@provides(key="heavy_service", lazy=True)
def heavy(self):
COUNTER["value"] += 1
return {"payload": "hello"}
container = init(__name__)
svc = container.get("heavy_service") # not created yet
print(COUNTER["value"]) # 0
print(svc["payload"]) # triggers creation
print(COUNTER["value"]) # 1
```
---
## π§ Dependency Resolution Order
1. parameter **name**
2. exact **type annotation**
3. **MRO fallback** (walk base classes)
4. `str(name)`
---
## β‘ Eager vs. Lazy (Blueprint Behavior)
At the end of `init()`, Pico-IoC performs a **blueprint**:
- **Eager** (`lazy=False`, default): instantiated immediately; failures stop startup.
- **Lazy** (`lazy=True`): returns a `ComponentProxy`; instantiated on first real use.
**Lifecycle:**
βββββββββββββββββββββββββ
β init() β
βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Scan & bind deps β
βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β Blueprint instantiates all β
β non-lazy (eager) beans β
βββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββ
β Container ready β
βββββββββββββββββββββββββ
**Best practice:** keep eager+fail-fast for production parity with Spring; use lazy only for heavy/optional deps or to support negative tests.
---
## π Migration Guide (v0.2.1 β v0.3.0)
* **Defaults changed:** `@component` and `@provides` now default to `lazy=False` (eager).
* **Proxy renamed:** `LazyProxy` β `ComponentProxy` (only relevant if referenced directly).
* **Tests/fixtures:** components intentionally missing deps should be marked `@component(lazy=True)` (to avoid failing `init()`), or excluded from the scan.
Example fix for an intentional failure case:
```python
@component(lazy=True)
class MissingDep:
def __init__(self, missing):
self.missing = missing
```
---
## π API Reference
### `init(root, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
Scan and bind components in `root` (str module name or module).
Skips the calling module if `auto_exclude_caller=True`.
Runs blueprint (instantiate all `lazy=False` bindings).
### `@component(cls=None, *, name=None, lazy=False)`
Register a class as a component.
Use `name` for a custom key.
Set `lazy=True` to defer creation.
### `@factory_component`
Mark a class as a component factory (its methods can `@provides` bindings).
### `@provides(key, *, lazy=False)`
Declare that a factory method provides a component under `key`.
Set `lazy=True` for deferred creation (`ComponentProxy`).
---
## π§ͺ Testing
```bash
pip install tox
tox -e py311
```
Tip: for βmissing dependencyβ tests, mark those components as `lazy=True` so `init()` remains fail-fast for real components while your test still asserts failure on resolution.
---
## π Extensibility: Plugins, Binder, and Lifecycle Hooks
From `v0.4.0` onward, Pico-IoC can be cleanly extended without patching the core.
This enables optional integration layers like `pico-ioc-rest` for Flask, FastAPI, etc., while keeping the core dependency-free.
### Plugin Protocol
A plugin is any object that implements some or all of the following methods:
```python
from pico_ioc import PicoPlugin, Binder
class MyPlugin:
def before_scan(self, package, binder: Binder): ...
def visit_class(self, module, cls, binder: Binder): ...
def after_scan(self, package, binder: Binder): ...
def after_bind(self, container, binder: Binder): ...
def before_eager(self, container, binder: Binder): ...
def after_ready(self, container, binder: Binder): ...
```
All hooks are optional. If present, they are called in this order during `init()`:
1. **before\_scan** β called before package scanning starts.
2. **visit\_class** β called for every class discovered during scanning.
3. **after\_scan** β called after scanning all modules.
4. **after\_bind** β called after the core has bound all components/factories.
5. **before\_eager** β called right before eager (non-lazy) instantiation.
6. **after\_ready** β called after all eager instantiation is complete.
### Binder API
Plugins receive a [`Binder`](#binder-api) instance in each hook, allowing them to:
* **bind**: register new providers in the container.
* **has**: check if a binding exists.
* **get**: resolve a binding immediately.
Example plugin that binds a βmarkerβ component when a certain class is discovered:
```python
class MarkerPlugin:
def visit_class(self, module, cls, binder):
if cls.__name__ == "SpecialService" and not binder.has("marker"):
binder.bind("marker", lambda: {"ok": True}, lazy=False)
container = init("my_app", plugins=(MarkerPlugin(),))
assert container.get("marker") == {"ok": True}
```
### Creating Extensions
With the plugin API, you can build separate packages like `pico-ioc-rest`:
```python
from pico_ioc import PicoPlugin, Binder, create_instance, resolve_param
from flask import Flask
class FlaskRestPlugin:
def __init__(self):
self.controllers = []
def visit_class(self, module, cls, binder: Binder):
if getattr(cls, "_is_controller", False):
self.controllers.append(cls)
def after_bind(self, container, binder: Binder):
app: Flask = container.get(Flask)
for ctl_cls in self.controllers:
ctl = create_instance(ctl_cls, container)
# register routes here using `resolve_param` for handler DI
```
### Public Helpers for Extensions
Plugins can reuse Pico-IoCβs DI logic without duplicating it:
* **`create_instance(cls, container)`** β instantiate a class with DI, respecting Pico-IoCβs resolution order.
* **`resolve_param(container, parameter)`** β resolve a single function/class parameter via Pico-IoC rules.
---
## β FAQ
**Q: Can I make the container lenient at startup?**
A: By design itβs strict. Prefer `lazy=True` on specific bindings or exclude problem modules from the scan.
**Q: Thread safety?**
A: Container uses `ContextVar` to guard re-entrancy during scanning. Singletons are created once per container; typical usage is in single-threaded app startup, then read-mostly.
**Q: Frameworks?**
A: Framework-agnostic. Works with Flask, FastAPI, CLIs, scripts, etc.
---
## π License
MIT β see [LICENSE](https://opensource.org/licenses/MIT)
Raw data
{
"_id": null,
"home_page": null,
"name": "pico-ioc",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "ioc, di, dependency injection, inversion of control, decorator",
"author": null,
"author_email": "David Perez Cabrera <dperezcabrera@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/f7/a7/cf3ea9c7b4cc2ab37d9dbf13406e23f3f3ff813eff438dbdee309ba7c08a/pico_ioc-0.4.0.tar.gz",
"platform": null,
"description": "# \ud83d\udce6 Pico-IoC: A Minimalist IoC Container for Python\n\n[](https://pypi.org/project/pico-ioc/)\n[](https://opensource.org/licenses/MIT)\n\n\n**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control container for Python.\nBuild loosely-coupled, testable apps without manual wiring. Inspired by the Spring ecosystem.\n\n---\n\n## \u2728 Key Features\n\n* **Zero dependencies** \u2014 pure Python.\n* **Decorator API** \u2014 `@component`, `@factory_component`, `@provides`.\n* **Auto discovery** \u2014 scans a package and registers components.\n* **Eager by default, fail-fast** \u2014 non-lazy bindings are instantiated immediately after `init()`. Missing deps fail startup.\n* **Opt-in lazy** \u2014 set `lazy=True` to defer creation (wrapped in `ComponentProxy`).\n* **Factories** \u2014 encapsulate complex creation logic.\n* **Smart resolution** \u2014 by **parameter name**, then **type annotation**, then **MRO fallback**, then **string(name)**.\n* **Re-entrancy guard** \u2014 prevents `get()` during scanning.\n* **Auto-exclude caller** \u2014 `init()` skips the calling module to avoid double scanning.\n\n---\n\n## \ud83d\udce6 Installation\n\n```bash\npip install pico-ioc\n```\n\n---\n\n## \ud83d\ude80 Quick Start\n\n```python\nfrom pico_ioc import component, init\n\n@component\nclass AppConfig:\n def get_db_url(self):\n return \"postgresql://user:pass@host/db\"\n\n@component\nclass DatabaseService:\n def __init__(self, config: AppConfig):\n self._cs = config.get_db_url()\n def get_data(self):\n return f\"Data from {self._cs}\"\n\ncontainer = init(__name__) # blueprint runs here (eager + fail-fast)\ndb = container.get(DatabaseService)\nprint(db.get_data())\n```\n\n---\n\n## \ud83e\udde9 Custom Component Keys\n\n```python\nfrom pico_ioc import component, init\n\n@component(name=\"config\") # custom key\nclass AppConfig:\n db_url = \"postgresql://user:pass@localhost/db\"\n\n@component\nclass Repository:\n def __init__(self, config: \"config\"): # resolve by NAME\n self.url = config.db_url\n\ncontainer = init(__name__)\nprint(container.get(\"config\").db_url)\n```\n\n---\n\n## \ud83c\udfed Factories and `@provides`\n\n* Default is **eager** (`lazy=False`). Eager bindings are constructed at the end of `init()`.\n* Use `lazy=True` for on-first-use creation via `ComponentProxy`.\n\n```python\nfrom pico_ioc import factory_component, provides, init\n\nCOUNTER = {\"value\": 0}\n\n@factory_component\nclass ServicesFactory:\n @provides(key=\"heavy_service\", lazy=True)\n def heavy(self):\n COUNTER[\"value\"] += 1\n return {\"payload\": \"hello\"}\n\ncontainer = init(__name__)\nsvc = container.get(\"heavy_service\") # not created yet\nprint(COUNTER[\"value\"]) # 0\nprint(svc[\"payload\"]) # triggers creation\nprint(COUNTER[\"value\"]) # 1\n```\n\n---\n\n## \ud83e\udde0 Dependency Resolution Order\n\n1. parameter **name**\n2. exact **type annotation**\n3. **MRO fallback** (walk base classes)\n4. `str(name)`\n\n---\n\n## \u26a1 Eager vs. Lazy (Blueprint Behavior)\n\nAt the end of `init()`, Pico-IoC performs a **blueprint**:\n\n- **Eager** (`lazy=False`, default): instantiated immediately; failures stop startup.\n- **Lazy** (`lazy=True`): returns a `ComponentProxy`; instantiated on first real use.\n\n**Lifecycle:**\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 init() \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Scan & bind deps \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Blueprint instantiates all \u2502\n \u2502 non-lazy (eager) beans \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Container ready \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n\n**Best practice:** keep eager+fail-fast for production parity with Spring; use lazy only for heavy/optional deps or to support negative tests.\n\n---\n\n## \ud83d\udd04 Migration Guide (v0.2.1 \u2192 v0.3.0)\n\n* **Defaults changed:** `@component` and `@provides` now default to `lazy=False` (eager).\n* **Proxy renamed:** `LazyProxy` \u2192 `ComponentProxy` (only relevant if referenced directly).\n* **Tests/fixtures:** components intentionally missing deps should be marked `@component(lazy=True)` (to avoid failing `init()`), or excluded from the scan.\n\nExample fix for an intentional failure case:\n\n```python\n@component(lazy=True)\nclass MissingDep:\n def __init__(self, missing):\n self.missing = missing\n```\n\n---\n\n## \ud83d\udee0 API Reference\n\n### `init(root, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`\n\nScan and bind components in `root` (str module name or module).\nSkips the calling module if `auto_exclude_caller=True`.\nRuns blueprint (instantiate all `lazy=False` bindings).\n\n### `@component(cls=None, *, name=None, lazy=False)`\n\nRegister a class as a component.\nUse `name` for a custom key.\nSet `lazy=True` to defer creation.\n\n### `@factory_component`\n\nMark a class as a component factory (its methods can `@provides` bindings).\n\n### `@provides(key, *, lazy=False)`\n\nDeclare that a factory method provides a component under `key`.\nSet `lazy=True` for deferred creation (`ComponentProxy`).\n\n---\n\n## \ud83e\uddea Testing\n\n```bash\npip install tox\ntox -e py311\n```\n\nTip: for \u201cmissing dependency\u201d tests, mark those components as `lazy=True` so `init()` remains fail-fast for real components while your test still asserts failure on resolution.\n\n---\n\n## \ud83d\udd0c Extensibility: Plugins, Binder, and Lifecycle Hooks\n\nFrom `v0.4.0` onward, Pico-IoC can be cleanly extended without patching the core.\nThis enables optional integration layers like `pico-ioc-rest` for Flask, FastAPI, etc., while keeping the core dependency-free.\n\n### Plugin Protocol\n\nA plugin is any object that implements some or all of the following methods:\n\n```python\nfrom pico_ioc import PicoPlugin, Binder\n\nclass MyPlugin:\n def before_scan(self, package, binder: Binder): ...\n def visit_class(self, module, cls, binder: Binder): ...\n def after_scan(self, package, binder: Binder): ...\n def after_bind(self, container, binder: Binder): ...\n def before_eager(self, container, binder: Binder): ...\n def after_ready(self, container, binder: Binder): ...\n```\n\nAll hooks are optional. If present, they are called in this order during `init()`:\n\n1. **before\\_scan** \u2014 called before package scanning starts.\n2. **visit\\_class** \u2014 called for every class discovered during scanning.\n3. **after\\_scan** \u2014 called after scanning all modules.\n4. **after\\_bind** \u2014 called after the core has bound all components/factories.\n5. **before\\_eager** \u2014 called right before eager (non-lazy) instantiation.\n6. **after\\_ready** \u2014 called after all eager instantiation is complete.\n\n### Binder API\n\nPlugins receive a [`Binder`](#binder-api) instance in each hook, allowing them to:\n\n* **bind**: register new providers in the container.\n* **has**: check if a binding exists.\n* **get**: resolve a binding immediately.\n\nExample plugin that binds a \u201cmarker\u201d component when a certain class is discovered:\n\n```python\nclass MarkerPlugin:\n def visit_class(self, module, cls, binder):\n if cls.__name__ == \"SpecialService\" and not binder.has(\"marker\"):\n binder.bind(\"marker\", lambda: {\"ok\": True}, lazy=False)\n\ncontainer = init(\"my_app\", plugins=(MarkerPlugin(),))\nassert container.get(\"marker\") == {\"ok\": True}\n```\n\n### Creating Extensions\n\nWith the plugin API, you can build separate packages like `pico-ioc-rest`:\n\n```python\nfrom pico_ioc import PicoPlugin, Binder, create_instance, resolve_param\nfrom flask import Flask\n\nclass FlaskRestPlugin:\n def __init__(self):\n self.controllers = []\n\n def visit_class(self, module, cls, binder: Binder):\n if getattr(cls, \"_is_controller\", False):\n self.controllers.append(cls)\n\n def after_bind(self, container, binder: Binder):\n app: Flask = container.get(Flask)\n for ctl_cls in self.controllers:\n ctl = create_instance(ctl_cls, container)\n # register routes here using `resolve_param` for handler DI\n```\n\n### Public Helpers for Extensions\n\nPlugins can reuse Pico-IoC\u2019s DI logic without duplicating it:\n\n* **`create_instance(cls, container)`** \u2014 instantiate a class with DI, respecting Pico-IoC\u2019s resolution order.\n* **`resolve_param(container, parameter)`** \u2014 resolve a single function/class parameter via Pico-IoC rules.\n\n---\n\n## \u2753 FAQ\n\n**Q: Can I make the container lenient at startup?**\nA: By design it\u2019s strict. Prefer `lazy=True` on specific bindings or exclude problem modules from the scan.\n\n**Q: Thread safety?**\nA: Container uses `ContextVar` to guard re-entrancy during scanning. Singletons are created once per container; typical usage is in single-threaded app startup, then read-mostly.\n\n**Q: Frameworks?**\nA: Framework-agnostic. Works with Flask, FastAPI, CLIs, scripts, etc.\n\n---\n\n## \ud83d\udcdc License\n\nMIT \u2014 see [LICENSE](https://opensource.org/licenses/MIT)\n\n\n",
"bugtrack_url": null,
"license": null,
"summary": "A minimalist, zero-dependency Inversion of Control (IoC) container for Python.",
"version": "0.4.0",
"project_urls": {
"Homepage": "https://github.com/dperezcabrera/pico-ioc",
"Issue Tracker": "https://github.com/dperezcabrera/pico-ioc/issues",
"Repository": "https://github.com/dperezcabrera/pico-ioc"
},
"split_keywords": [
"ioc",
" di",
" dependency injection",
" inversion of control",
" decorator"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "ed939f684f9d1abc3f6b330869af9293e6e34ac942722018e3cbec1a4543fe3a",
"md5": "1441b048dd4e354d29b3ac4560cd106a",
"sha256": "9c8f821cb4ecec6c97e68b7428946de9a159a5da6e475ae4b065f9c3791ecada"
},
"downloads": -1,
"filename": "pico_ioc-0.4.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "1441b048dd4e354d29b3ac4560cd106a",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 8295,
"upload_time": "2025-08-10T14:44:49",
"upload_time_iso_8601": "2025-08-10T14:44:49.409976Z",
"url": "https://files.pythonhosted.org/packages/ed/93/9f684f9d1abc3f6b330869af9293e6e34ac942722018e3cbec1a4543fe3a/pico_ioc-0.4.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "f7a7cf3ea9c7b4cc2ab37d9dbf13406e23f3f3ff813eff438dbdee309ba7c08a",
"md5": "4d0aff491ebed386ee217407693b05b8",
"sha256": "5515257aa42bb27ffe650c9bb86e8562303a4983fda8264d2e64c04e8d405926"
},
"downloads": -1,
"filename": "pico_ioc-0.4.0.tar.gz",
"has_sig": false,
"md5_digest": "4d0aff491ebed386ee217407693b05b8",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 14941,
"upload_time": "2025-08-10T14:44:50",
"upload_time_iso_8601": "2025-08-10T14:44:50.717916Z",
"url": "https://files.pythonhosted.org/packages/f7/a7/cf3ea9c7b4cc2ab37d9dbf13406e23f3f3ff813eff438dbdee309ba7c08a/pico_ioc-0.4.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-10 14:44:50",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "dperezcabrera",
"github_project": "pico-ioc",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"tox": true,
"lcname": "pico-ioc"
}