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

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

Source: Dev.to

What is InjectQ? ## Install in one line ## Quick Start - Zero config, maximum clarity ## Core Features ## πŸ”§ Dict-like API ## 🎯 Decorator + Type-based Injection ## πŸ”„ Scopes and Lifetimes ## πŸ†• Hybrid Factories - The Feature That Changes Everything ## πŸš€ FastAPI Integration ## πŸ§ͺ Testing β€” Mocking Without the Pain ## πŸ—οΈ Modules and Providers ## πŸ›‘οΈ Abstract Class Validation ## Performance Benchmarks ## Why InjectQ Over Alternatives? 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. "We wanted DI that feels like Python - not like a Java framework that got lost on its way to PyPI." InjectQ is a modern, lightweight Python dependency injection library focused on: For framework integrations: That's it. @singleton = one instance app-wide. @inject = auto-resolve from type hints. No XML, no 500-line config, no magic. The simplest mental model possible: Also supports Inject[T] for inline type annotations that work with static type checkers. 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: New way with invoke(): InjectQ resolves what it knows from the container. You provide only the runtime-specific values. Clean. Pro tip: Use InjectQ.test_mode() with pytest fixtures to auto-reset your container between tests. For larger apps, organize your bindings into modules: InjectQ validates at bind time, not at resolution time: Fail fast. Debug less. InjectQ isn't just clean β€” it's fast. Thread-safe by default. Production-ready from day one. 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. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: pip install injectq Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: pip install injectq COMMAND_BLOCK: pip install injectq COMMAND_BLOCK: pip install injectq[fastapi] # FastAPI support pip install injectq[taskiq] # Taskiq support Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: pip install injectq[fastapi] # FastAPI support pip install injectq[taskiq] # Taskiq support COMMAND_BLOCK: pip install injectq[fastapi] # FastAPI support pip install injectq[taskiq] # Taskiq support COMMAND_BLOCK: 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 COMMAND_BLOCK: 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! COMMAND_BLOCK: 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! COMMAND_BLOCK: 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 COMMAND_BLOCK: from injectq import InjectQ container = InjectQ.get_instance() # Bind anything container[str] = "config_value" container[Database] = Database() # Retrieve db = container[Database] COMMAND_BLOCK: from injectq import InjectQ container = InjectQ.get_instance() # Bind anything container[str] = "config_value" container[Database] = Database() # Retrieve db = container[Database] COMMAND_BLOCK: @inject def process(service: UserService, db: Database): # Both auto-resolved from the container ... Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @inject def process(service: UserService, db: Database): # Both auto-resolved from the container ... COMMAND_BLOCK: @inject def process(service: UserService, db: Database): # Both auto-resolved from the container ... COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 βœ“ COMMAND_BLOCK: 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 βœ“ COMMAND_BLOCK: # ❌ 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 COMMAND_BLOCK: # ❌ Old way β€” manually resolve everything db = container[Database] cache = container[Cache] svc = container.call_factory("user_service", db, cache, "user123") COMMAND_BLOCK: # ❌ Old way β€” manually resolve everything db = container[Database] cache = container[Cache] svc = container.call_factory("user_service", db, cache, "user123") COMMAND_BLOCK: 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 COMMAND_BLOCK: 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) COMMAND_BLOCK: 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) COMMAND_BLOCK: 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 COMMAND_BLOCK: 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) COMMAND_BLOCK: 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) COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 COMMAND_BLOCK: 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()]) COMMAND_BLOCK: 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()]) COMMAND_BLOCK: 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 COMMAND_BLOCK: 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) COMMAND_BLOCK: 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) COMMAND_BLOCK: pip install injectq Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: pip install injectq COMMAND_BLOCK: pip install injectq - βœ… Clarity and simplicity - βœ… Type safety (works with mypy, pyright) - βœ… Async-first APIs - βœ… Seamless FastAPI & Taskiq integration - βœ… Production-grade performance (270ns per bind) - Manually wiring dependencies - Global state leaking into tests - Framework integrations that require 200 lines of glue code - πŸ™ GitHub β€” star us if this helps! - πŸ“– Full Documentation