Ididi
the fastest and most advanced dependency injection lib in Python, used by lihil.
Install / Use
/learn @raceychan/IdidiREADME
Ididi
A type-based, full-fledged dependency injection library.
ididi is 100% test covered and strictly typed.
📚 Docs: : <a href="https://raceychan.github.io/ididi" target="_blank"> https://raceychan.github.io/ididi </a>
Install
ididi requires python >= 3.9
pip install ididi
To view viusal dependency graph, install graphviz
pip install ididi[graphviz]
Features
- Powerful: Ididi does what alternatives do, and also provides features that others don't.
- Performant: almost as fast as hard-coded factories, one of the fastest dependency injection framework available.
- Noninvasive: No / minial changes to your existing code
- Smart: inject dependency based on type hints, with strong support to
typingmodule. - Correct, strictly typed, well-organized exceptions, well-formatted and detail-rich error messages
TypingSupport
ididi has strong support to typing module, includes:
- TypedDict
- Unpack
- NewType
- Annotated
- Literal
- Optional
- Union
...and more.
Check out tests/features/test_typing_support.py for examples.
Choosing reuse strategy for typed dependencies
- Dependency type used as singleton →
reuse=True - Dependency type with limited instances → wrap the target with
typing.NewType - Dependency used as transient →
reuse=False
Example: solving limited agent types with NewType
When only a handful of instances of the same type should be resolvable, wrap each variant in a distinct NewType. This keeps the factories strongly typed without turning everything into a singleton.
from typing import NewType
from ididi import Graph
class LLMService: ...
class Agent:
def __init__(self, prompt: str, llm: LLMService):
self.prompt = prompt
self.llm = llm
MathAgent = NewType("MathAgent", Agent)
EnglishAgent = NewType("EnglishAgent", Agent)
def math_agent_factory(llm: LLMService) -> MathAgent:
return MathAgent(Agent(prompt="Solve math problems", llm=llm))
def english_agent_factory(llm: LLMService) -> EnglishAgent:
return EnglishAgent(Agent(prompt="Assist with English tasks", llm=llm))
dg = Graph()
math_agent = dg.resolve(math_agent_factory)
assert isinstance(math_agent, Agent) and math_agent.prompt == "Solve math problems"
english_agent = dg.resolve(english_agent_factory)
assert isinstance(english_agent, Agent) and english_agent.prompt == "Assist with English tasks"
See tests/features/test_typing_support.py::test_solve_agent_new_types for the full test case.
Usage
Quick Start
from typing import AsyncGenerator
from ididi import use, entry
async def conn_factory(engine: AsyncEngine) -> AsyncGenerator[AsyncConnection, None]:
async with engine.begin() as conn:
yield conn
class UnitOfWork:
def __init__(self, conn: Annotated[AsyncConnection, use(conn_factory)]):
self._conn = conn
@entry
async def main(command: CreateUser, uow: UnitOfWork):
await uow.execute(build_query(command))
# note uow is automatically injected here
await main(CreateUser(name='user'))
To resolve AsyncConnection outside of entry function
from ididi import Graph
dg = Graph()
async with dg.ascope():
conn = await scope.resolve(conn_factory)
Key Concepts
Marks
ididi provides two marks, use and Ignore, they are convenient shortcut for Graph.node.
Technically, they are just metadata carried by typing.Annotated, and should work fine with other Annotated metadata.
use
You can use Graph.node to register a dependent with its factory,
here we register dependent Database with its factory db_factory.
This means whenver we call dg.resolve(Database), db_factory will be call.
def db_factory() -> Database:
return Database()
dg = Graph()
dg.node(db_factory)
Alternatively, you can annotate it inside __init__, this allow you to instantiate
Graph in a lazy manner.
from ididi import use
class Repository:
def __init__(self, db: Annotated[Database, use(db_factory)]):
...
Ignore
For dependencies you would like ididi to ignore, you can config ididi to ignore them.
- Ignore at Graph level
from datetime import datetime
from pathlib import Path
dg = Graph(ignore=(datetime, Path))
- Ignore at Node level
dg = Graph()
class Clock:
def __init__(self, dt: datetime): ...
dg.node(Clock, ignore=datetime)
Alternatively, you can mark a dependency using ididi.Ignore,
from ididi import Ignore
class Clock:
def __init__(self, dt: Ignore[datetime]): ...
Function dependency
Declear a function as a dependency by using Ignore to annotate its return type.
@dataclass
class User:
name: str
role: str
def get_user(config: Config) -> Ignore[User]:
assert isinstance(config, Config)
return User("user", "admin")
def validate_admin(
user: Annotated[User, use(get_user)], service: UserService
) -> Ignore[str]:
assert user.role == "admin"
assert isinstance(service, UserService)
return "ok"
class Route:
def __init__(self, validte_permission: Annotated[str, use(validate_admin)]):
assert validte_permission == "ok"
assert dg.resolve(validate_admin) == "ok"
assert isinstance(dg.resolve(Route), Route)
Since get_user returns Ignore[User] instead of User, it won't be used as factory to resolve User.
Dependency factory
from ididi import use
from typing import NewType
from datetime import datetime, timezone
UserID = NewType("UserID", str)
def utc_factory() -> datetime:
return datetime.now(timezone.utc)
def user_id_factory() -> UserID:
return UserID(str(uuid4()))
class User:
def __init__(self, user_id: UserID, created_at: Annotated[datetime, use(utc_factory)]):
self.user_id = user_id
self.created_at = created_at
user = ididi.resolve(User)
assert user.created_at.tzinfo == timezone.utc
[!TIP]
Graph.nodeaccepts a wide arrange of types, such as dependent class, sync/async facotry, sync/async resource factory, with typing support.
Scope
Scope is a temporary view of the graph specialized for handling resources.
In a nutshell:
- Scope can access(read-only) registered singletons and resolved instances of its parent graph
- Dependents registered/resolved in a scope will stay in the scope.
- its parent graph can't access its registered singletons and resolved resources.
Using Scope to manage resources
- Infinite number of nested scope
- Parent scope can be accssed by its child scopes(within the same context)
- Resources will be shared across dependents only withint the same scope
- Resources will be automatically closed when the scope is exited.
- Classes that implment
contextlib.AbstractContextManagerorcontextlib.AbstractAsyncContextManagerare also considered to be resources and can/should be resolved within scope. - Scopes are separated by context
[!TIP] If you have two call stack of
a1 -> b1anda2 -> b2, Herea1anda2are two calls to the same functiona, then, inb1, you can only access scope created by thea1, nota2.
This is particularly useful when you try to separate resources by route, endpoint, request, etc.
Async scope
@dg.node
def get_resource() -> ty.Generator[Resource, None, None]:
res = Resource()
with res:
yield res
@dg.node
async def get_asyncresource() -> ty.Generator[AsyncResource, None, None]:
res = AsyncResource()
async with res:
yield res
with dg.scope() as scope:
resource = scope.resolve(Resource)
# For async generator
async with dg.ascope() as scope:
resource = await scope.resolve(AsyncResource)
[!TIP]
dg.nodewill leave your class/factory untouched, i.e., you can use it as a function. e.g.dg.node(get_resource, reuse=False)
Contexted Scope
You can use dg.use_scope to retrive most recent scope, context-wise, this allows your to have access the scope without passing it around, e.g.
async def service_factory():
async with dg.ascope() as scope:
service = scope.resolve(Service)
yield service
@app.get("users")
async def get_user(service: Service = Depends(service_factory))
await service.create_user(...)
Then somewhere deep in your service.create_user call stack
async def create_and_publish():
uow = dg.use_scope().resolve(UnitOfWork)
async with uow.trans():
user_repo.add_user(user)
event_store.add(user_created_event)
Here dg.use_scope() would return the same scope you created in your service_factory.
Named Scope
You can create infinite level of scopes by assigning hashable name to scopes
# at the top most entry of a request
async with dg.ascope(request_id) as scope:
...
now scope with name request_id is accessible everywhere within the request context
request_scope = dg.use_scope(request_id)
[!NOTE] Two or more scopes with the same name would follow most recent rule.
Nested Nmaed Scope
async with dg.ascope(app_name) as app_scope:
async with dg.ascope(router_name) as router_scope:
async with dg.ascope(endpoint_name) as endpoint_scope:
async with dg.ascope(user_id) as user_scope:
async with dg.ascope(request
Related Skills
node-connect
347.6kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
claude-opus-4-5-migration
108.4kMigrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5
frontend-design
108.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
model-usage
347.6kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
