DEV Community

Cover image for Stop Wiring Dependencies by Hand - Meet InjectQ, Python DI Done Right
Mothilal M for 10XScale

Posted on

Stop Wiring Dependencies by Hand - Meet InjectQ, Python DI Done Right

You've been there.

A service that needs a database, which needs a config, which needs an env variable that someone hardcoded three months ago and nobody remembers where. You're passing objects down ten layers of constructors. Testing means faking half your app.

It's messy. And it doesn't have to be.

Dependency Injection is the fix - but most Python DI libraries feel like they were designed for a different language. Overly complex, decorator-heavy, or magical in ways that make debugging a nightmare.

So we built InjectQ.

"We wanted DI that feels like Python - not like a Java framework that got lost on its way to PyPI."


What is InjectQ?

InjectQ is a modern, lightweight Python dependency injection library focused on:

  • ✅ Clarity and simplicity
  • ✅ Type safety (works with mypy, pyright)
  • ✅ Async-first APIs
  • ✅ Seamless FastAPI & Taskiq integration
  • ✅ Production-grade performance (270ns per bind)

Links:


Install in one line

pip install injectq
Enter fullscreen mode Exit fullscreen mode

For framework integrations:

pip install injectq[fastapi]   # FastAPI support
pip install injectq[taskiq]    # Taskiq support
Enter fullscreen mode Exit fullscreen mode

Quick Start - Zero config, maximum clarity

from injectq import InjectQ, inject, singleton

container = InjectQ.get_instance()

# Bind a value — dict-style, no ceremony
container[str] = "Hello, World!"

@singleton
class UserService:
    def __init__(self, message: str):
        self.message = message

    def greet(self) -> str:
        return f"Service says: {self.message}"

@inject
def main(service: UserService) -> None:
    print(service.greet())

main()  # → Service says: Hello, World!
Enter fullscreen mode Exit fullscreen mode

That's it. @singleton = one instance app-wide. @inject = auto-resolve from type hints. No XML, no 500-line config, no magic.


Core Features

🔧 Dict-like API

The simplest mental model possible:

from injectq import InjectQ

container = InjectQ.get_instance()

# Bind anything
container[str] = "config_value"
container[Database] = Database()

# Retrieve
db = container[Database]
Enter fullscreen mode Exit fullscreen mode

🎯 Decorator + Type-based Injection

@inject
def process(service: UserService, db: Database):
    # Both auto-resolved from the container
    ...
Enter fullscreen mode Exit fullscreen mode

Also supports Inject[T] for inline type annotations that work with static type checkers.


🔄 Scopes and Lifetimes

from injectq import singleton, transient, scoped

@singleton
class Database: ...       # One instance, lives forever

@transient
class Validator: ...      # New instance every resolution

@scoped("request")
class RequestContext: ... # One per request scope

# Async scopes work too
async with container.scope("request"):
    ctx1 = container.get(RequestContext)
    ctx2 = container.get(RequestContext)
    assert ctx1 is ctx2  # Same instance ✓
Enter fullscreen mode Exit fullscreen mode

🆕 Hybrid Factories - The Feature That Changes Everything

This is new in v0.4 and it's genuinely great.

The problem: You have a factory that needs a database (DI-managed) and a user ID (runtime value). Old way is verbose:

# ❌ Old way — manually resolve everything
db    = container[Database]
cache = container[Cache]
svc   = container.call_factory("user_service", db, cache, "user123")
Enter fullscreen mode Exit fullscreen mode

New way with invoke():

def create_user_service(db: Database, cache: Cache, user_id: str):
    return UserService(db, cache, user_id)

container.bind_factory("user_service", create_user_service)

# ✅ Auto-inject db and cache, you only pass what you know
svc = container.invoke("user_service", user_id="user123")

# Async version
svc = await container.ainvoke("async_service", batch_size=100)
Enter fullscreen mode Exit fullscreen mode

InjectQ resolves what it knows from the container. You provide only the runtime-specific values. Clean.


🚀 FastAPI Integration

from typing import Annotated
from fastapi import FastAPI
from injectq import InjectQ, singleton
from injectq.integrations.fastapi import setup_fastapi, InjectFastAPI

app       = FastAPI()
container = InjectQ.get_instance()
setup_fastapi(container, app)

@singleton
class UserService:
    def get_user(self, user_id: int) -> dict:
        return {"id": user_id}

@app.get("/users/{user_id}")
async def get_user(
    user_id: int,
    user_service: Annotated[UserService, InjectFastAPI(UserService)],
):
    return user_service.get_user(user_id)
Enter fullscreen mode Exit fullscreen mode

🧪 Testing — Mocking Without the Pain

from injectq.testing import override_dependency, test_container

# Override a specific dep for the duration of a block
with override_dependency(Database, MockDatabase()):
    service = container.get(UserService)
    # UserService gets MockDatabase here ✓

# Fully isolated test container — no global state bleed
with test_container() as tc:
    tc.bind(Database, MockDatabase)
    # Clean slate for each test
Enter fullscreen mode Exit fullscreen mode

Pro tip: Use InjectQ.test_mode() with pytest fixtures to auto-reset your container between tests.


🏗️ Modules and Providers

For larger apps, organize your bindings into modules:

from injectq.modules import Module, SimpleModule, ProviderModule, provider

class AppModule(Module):
    def configure(self, binder):
        binder.bind(Config, Config())
        binder.bind(Database, Database)

class Providers(ProviderModule):
    @provider
    def make_notifier(self, db: Database, cfg: Config) -> Notifier:
        return Notifier(db, cfg)

container = InjectQ(modules=[AppModule(), Providers()])
Enter fullscreen mode Exit fullscreen mode

🛡️ Abstract Class Validation

InjectQ validates at bind time, not at resolution time:

from abc import ABC, abstractmethod
from injectq.utils.exceptions import BindingError

class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> str: ...

# ❌ Raises BindingError immediately — no surprises at runtime
container.bind(PaymentProcessor, PaymentProcessor)

# ✅ Correct — bind the concrete implementation
container.bind(PaymentProcessor, CreditCardProcessor)
Enter fullscreen mode Exit fullscreen mode

Fail fast. Debug less.


Performance Benchmarks

InjectQ isn't just clean — it's fast.

Operation Speed
Basic bind / get 270–780 nanoseconds
Dependency resolution ~1 microsecond
10-service web request simulation 142 microseconds
1,000+ concurrent operations Sub-millisecond

Thread-safe by default. Production-ready from day one.


Why InjectQ Over Alternatives?

Feature InjectQ dependency-injector injector
Dict-like API
FastAPI integration ✅ Built-in ❌ Manual
Hybrid factories (invoke)
Async scope contexts ⚠️ Limited
Testing utilities ✅ Built-in ❌ Manual
Taskiq integration
Abstract class guard ✅ Bind-time ❌ Runtime ❌ Runtime

TL;DR

If you're tired of:

  • Manually wiring dependencies
  • Global state leaking into tests
  • Framework integrations that require 200 lines of glue code

InjectQ is for you.

pip install injectq
Enter fullscreen mode Exit fullscreen mode

Built with ♥ by the 10xHub team. MIT Licensed. Contributions welcome.


Have questions or feature requests? Drop them in the comments or open an issue on GitHub. We read everything.

Top comments (0)