Skip to main content

Python Dependency Injection Practice Problems & Exercises

Practice: Dependency Injection

11 problems3 Easy4 Medium4 Hard60–90 min
← Back to lesson

Easy

#1Manual Constructor InjectionEasy
constructor-injectiondi-basics

Refactor a tightly-coupled OrderService to use constructor injection.

Python
# Before: tightly coupled (bad)
class TightOrderService:
    def __init__(self):
        self._stock = StockService()    # creates its own dependencies
        self._email = EmailService()

# After: constructor injection (good)
class StockService:
    def update(self, item: str, qty: int) -> None:
        print(f"Inventory updated for {item}")

class EmailService:
    def send(self, to: str, message: str) -> None:
        print(f"Email sent to {to}: {message}")

class OrderService:
    def __init__(self, stock: StockService, email: EmailService):
        self._stock = stock
        self._email = email
        self._next_id = 1

    def place(self, customer: str, item: str, qty: int, unit_price: int) -> int:
        order_id = self._next_id
        self._next_id += 1
        total = qty * unit_price
        print(f"Order {order_id}: {item} x {qty} = {total} cents")
        self._stock.update(item, qty)
        self._email.send(customer, f"Order {order_id} placed")
        return order_id

# Composition root
stock = StockService()
email = EmailService()
svc = OrderService(stock, email)
svc.place("[email protected]", "Widget", 2, 999)
Solution

The solution above demonstrates the core principle: dependencies are pushed in, not pulled out. OrderService.__init__ declares what it needs; the caller decides what to provide.

Three injection styles:

# 1. Constructor injection (preferred)
class Service:
def __init__(self, dep: IDep): self._dep = dep

# 2. Method injection (for optional/per-call deps)
class Service:
def process(self, dep: IDep): dep.do_work()

# 3. Property injection (rare, for optional deps)
class Service:
@property
def dep(self): return self._dep
@dep.setter
def dep(self, v): self._dep = v

Constructor injection is preferred because dependencies are explicit, required at construction time, and the object is always in a valid state.

Expected Output
Order 1: Widget x 2 = 1998 cents\nInventory updated for Widget\nEmail sent to [email protected]: Order 1 placed
Hints

Hint 1: Constructor injection means passing dependencies as arguments to __init__, not creating them inside the class.

Hint 2: The class should not call StockService() or EmailService() internally — it receives already-constructed instances.

#2Test the Injected ServiceEasy
testingfakedi-testing

Write fake implementations of StockService and EmailService, inject them into OrderService, and assert on the recorded calls.

Python
from dataclasses import dataclass, field

class StockService:
    def update(self, item: str, qty: int) -> None:
        print(f"Stock updated: {item} -{qty}")

class EmailService:
    def send(self, to: str, message: str) -> None:
        print(f"Email to {to}: {message}")

class OrderService:
    def __init__(self, stock: StockService, email: EmailService):
        self._stock = stock
        self._email = email
        self._next_id = 1

    def place(self, customer: str, item: str, qty: int, price: int) -> int:
        order_id = self._next_id
        self._next_id += 1
        self._stock.update(item, qty)
        self._email.send(customer, f"Order {order_id} placed")
        return order_id

# Fakes
@dataclass
class FakeStockService:
    calls: list = field(default_factory=list)
    def update(self, item: str, qty: int) -> None:
        self.calls.append((item, qty))

@dataclass
class FakeEmailService:
    sent: list = field(default_factory=list)
    def send(self, to: str, message: str) -> None:
        self.sent.append((to, message))

# Tests
fake_stock = FakeStockService()
fake_email = FakeEmailService()
svc = OrderService(fake_stock, fake_email)

order_id = svc.place("[email protected]", "TestItem", 3, 100)
assert order_id == 1, "Expected order ID 1"
print("Test 1 passed: order created with ID 1")

assert fake_stock.calls == [("TestItem", 3)]
print("Test 2 passed: stock updated for TestItem")

assert fake_email.sent[0][0] == "[email protected]"
print("Test 3 passed: email sent to [email protected]")
Solution

The solution is above. This demonstrates the primary testing benefit of DI:

Without DI: Testing OrderService would call real SMTP servers and real database writes. Tests are slow, fragile, and require infrastructure setup.

With DI: You inject FakeEmailService — no SMTP, no database. Tests run in microseconds and are completely isolated.

Fake vs Mock distinction:

  • Fake: A real implementation that works in-memory (like FakeEmailService that stores sent emails in a list).
  • Mock: A generated test double that records calls and can verify them (like unittest.mock.MagicMock).

Fakes are preferred for repository/service dependencies because they model the real behavior rather than just recording calls.

Expected Output
Test 1 passed: order created with ID 1\nTest 2 passed: stock updated for TestItem\nTest 3 passed: email sent to [email protected]
Hints

Hint 1: Create fake implementations of the dependencies that record calls instead of actually doing the work.

Hint 2: Inject the fakes into OrderService — the class should work identically regardless of which implementations it receives.

#3Spot the Anti-PatternsEasy
anti-patternsdiservice-locator

Identify all Dependency Injection anti-patterns in the code below and explain how to fix each one.

import json

# Global registry (service locator)
_REGISTRY = {}
def register(name, instance): _REGISTRY[name] = instance
def get_service(name): return _REGISTRY[name]

class ReportService:
def __init__(self):
# Anti-pattern 1: creates its own database connection
self._db = DatabaseConnection("postgresql://localhost/prod")

# Anti-pattern 2: reads config from a hard-coded file
with open("/etc/app/config.json") as f:
self._config = json.load(f)

# Anti-pattern 3: looks up a dependency from a global registry
self._mailer = get_service("mailer")

def generate(self, report_id: int):
# Anti-pattern 4: creates a collaborator mid-method
formatter = PdfFormatter()
data = self._db.query(f"SELECT * FROM reports WHERE id = {report_id}")
return formatter.format(data)
Solution

Anti-pattern 1: DatabaseConnection(...) inside __init__

Problem: You can never test ReportService without a real PostgreSQL server. You cannot swap to SQLite for tests.

Fix:

class ReportService:
def __init__(self, db: IDatabasePort): # injected
self._db = db

Anti-pattern 2: Hard-coded file path

Problem: Tests break unless /etc/app/config.json exists on every machine. Config is hidden inside the class.

Fix:

class ReportService:
def __init__(self, db: IDatabasePort, config: dict):
self._config = config
# Caller loads config and passes it in

Anti-pattern 3: Service locator get_service("mailer")

Problem: The dependency is hidden — you cannot tell from the class signature that ReportService needs a mailer. Tests must pre-populate the global registry. This is called the "service locator anti-pattern."

Fix:

class ReportService:
def __init__(self, db: IDatabasePort, mailer: IMailerPort):
self._mailer = mailer

Anti-pattern 4: PdfFormatter() created mid-method

Problem: Cannot switch to HtmlFormatter or CsvFormatter without editing the class. Cannot test with a fake formatter.

Fix:

class ReportService:
def __init__(self, db: IDatabasePort, formatter: IFormatterPort):
self._formatter = formatter

Corrected version:

class ReportService:
def __init__(self, db: IDatabasePort, config: dict, mailer: IMailerPort, formatter: IFormatterPort):
self._db = db
self._config = config
self._mailer = mailer
self._formatter = formatter

All four dependencies are now explicit, injectable, and testable.

Expected Output
See solution for analysis
Hints

Hint 1: Look for: new() calls inside constructors, global state access, service locator pattern, and hidden dependencies.

Hint 2: Each anti-pattern makes the class harder to test. Explain what makes each one a problem.


Medium

#4Build a Simple DI ContainerMedium
di-containerregistrywiring

Build a minimal DI container that supports singleton-scoped services and factory functions.

Python
from typing import Callable, Any

class Container:
    def __init__(self):
        self._factories: dict[str, Callable] = {}
        self._singletons: dict[str, Any] = {}

    def register(self, name: str, factory: Callable[["Container"], Any]) -> None:
        self._factories[name] = factory

    def resolve(self, name: str) -> Any:
        if name not in self._singletons:
            if name not in self._factories:
                raise KeyError(f"No registration for: {name}")
            self._singletons[name] = self._factories[name](self)
        return self._singletons[name]

# Services
class StockService:
    def update(self, item: str, qty: int) -> None:
        pass  # real impl would write to DB

class EmailService:
    def send(self, to: str, msg: str) -> None:
        print(f"Email to {to}: {msg}")

class OrderService:
    def __init__(self, stock: StockService, email: EmailService):
        self._stock = stock
        self._email = email
        self._next_id = 1

    def place(self, customer: str, item: str, qty: int) -> int:
        order_id = self._next_id
        self._next_id += 1
        self._stock.update(item, qty)
        self._email.send(customer, f"Order {order_id} placed")
        return order_id

# Register
container = Container()
container.register("stock", lambda c: StockService())
container.register("email", lambda c: EmailService())
container.register("order_svc", lambda c: OrderService(c.resolve("stock"), c.resolve("email")))

# Resolve
svc = container.resolve("order_svc")
print("OrderService resolved")
print(f"StockService is singleton: {container.resolve('stock') is container.resolve('stock')}")
print(f"EmailService is singleton: {container.resolve('email') is container.resolve('email')}")
svc.place("[email protected]", "Gadget", 1)
Solution

The solution is above. The container has three responsibilities:

  1. Store factory functions keyed by name.
  2. Resolve dependencies by calling the factory on first access.
  3. Cache resolved instances (singleton scope).

Adding transient scope:

def register_transient(self, name: str, factory: Callable) -> None:
self._transient_factories[name] = factory

def resolve_transient(self, name: str) -> Any:
return self._transient_factories[name](self)

Production DI containers for Python: dependency-injector, lagom, punq. They add type-based resolution, scope management, and lazy initialization.

from typing import Callable, TypeVar, Type

T = TypeVar("T")

class Container:
    """A minimal dependency injection container.
    Supports:
    - register(name, factory_fn)
    - resolve(name) -> instance (singleton by default)
    """
    pass
Expected Output
OrderService resolved\nStockService is singleton: True\nEmailService is singleton: True\nOrder placed for [email protected]
Hints

Hint 1: Store factories in a dict. On first resolve(), call the factory and cache the result. Subsequent calls return the cached instance (singleton scope).

Hint 2: The factory function takes the container as its argument so it can resolve its own dependencies: lambda c: OrderService(c.resolve("stock"), c.resolve("email")).

#5Scoped DependenciesMedium
scopedsingletontransientdi-scopes

Implement a ScopedContainer with singleton, transient, and scoped (per-request) dependency lifetimes.

Python
from dataclasses import dataclass, field
from typing import Callable, Any
import contextlib

class ScopedContainer:
    def __init__(self):
        self._singleton_factories: dict[str, Callable] = {}
        self._transient_factories: dict[str, Callable] = {}
        self._scoped_factories: dict[str, Callable] = {}
        self._singletons: dict[str, Any] = {}
        self._scope_cache: dict[str, Any] = {}

    def singleton(self, name: str, factory: Callable) -> None:
        self._singleton_factories[name] = factory

    def transient(self, name: str, factory: Callable) -> None:
        self._transient_factories[name] = factory

    def scoped(self, name: str, factory: Callable) -> None:
        self._scoped_factories[name] = factory

    def resolve(self, name: str) -> Any:
        if name in self._singleton_factories:
            if name not in self._singletons:
                self._singletons[name] = self._singleton_factories[name]()
            return self._singletons[name]
        if name in self._transient_factories:
            return self._transient_factories[name]()
        if name in self._scoped_factories:
            if name not in self._scope_cache:
                self._scope_cache[name] = self._scoped_factories[name]()
            return self._scope_cache[name]
        raise KeyError(f"Unknown: {name}")

    @contextlib.contextmanager
    def create_scope(self):
        old_cache = self._scope_cache
        self._scope_cache = {}
        try:
            yield self
        finally:
            self._scope_cache = old_cache

class Config:
    def __init__(self): self.id = id(self)

class DbSession:
    def __init__(self): self.id = id(self)

c = ScopedContainer()
c.singleton("config", lambda: Config())
c.transient("session", lambda: DbSession())
c.scoped("request_ctx", lambda: DbSession())

print(f"Singleton same instance: {c.resolve('config') is c.resolve('config')}")
print(f"Transient same instance: {c.resolve('session') is c.resolve('session')}")

with c.create_scope() as s1:
    ctx_a = s1.resolve("request_ctx")
    ctx_b = s1.resolve("request_ctx")
    print(f"Scoped same in scope: {ctx_a is ctx_b}")

with c.create_scope() as s2:
    ctx_c = s2.resolve("request_ctx")
    print(f"Scoped different across scopes: {ctx_a is not ctx_c}")
Solution

The solution is above. Understanding scopes is critical in web frameworks:

Web framework analogy:

  • Singleton: Database connection pool, configuration object. Created once at startup.
  • Transient: Stateless helper, calculator. Fresh instance every time — safe to share state temporarily.
  • Scoped: Database session, request context. One per HTTP request — created at request start, disposed at request end.

FastAPI's equivalent:

# Singleton
db_pool = create_pool()

# Transient
async def get_formatter():
return JsonFormatter() # new each time

# Scoped (per-request)
async def get_db():
session = SessionLocal()
try:
yield session
finally:
session.close()
from dataclasses import dataclass, field
from typing import Callable, Any
import uuid

class ScopedContainer:
    """Container with three scopes:
    - singleton: one instance for the app lifetime
    - transient: new instance every resolve
    - scoped: one instance per 'scope' context manager
    """
    pass
Expected Output
Singleton same instance: True\nTransient same instance: False\nScoped same in scope: True\nScoped different across scopes: True
Hints

Hint 1: For singleton scope, cache in a class-level dict. For transient, always call the factory. For scoped, use a context manager that creates a local cache dict and clears it on exit.

Hint 2: Use a threading.local() or a simple dict reset in __exit__ to manage scoped state.

#6Decorator-Based DIMedium
decoratordiautowiring

Implement an @inject decorator that automatically fills unset function parameters from a global service container.

Python
from typing import Callable, Any
import inspect
from functools import wraps

_CONTAINER: dict[str, Any] = {}

def provide(name: str, instance: Any) -> None:
    _CONTAINER[name] = instance

def inject(func: Callable) -> Callable:
    params = list(inspect.signature(func).parameters.keys())

    @wraps(func)
    def wrapper(*args, **kwargs):
        # Auto-fill missing keyword args from container
        for param in params[len(args):]:
            if param not in kwargs and param in _CONTAINER:
                kwargs[param] = _CONTAINER[param]
        return func(*args, **kwargs)

    return wrapper

# Services
class InventoryService:
    def check(self, item_id: int) -> bool:
        print("Inventory checked")
        return True

class NotificationService:
    def send(self, email: str) -> None:
        print(f"Notification sent to {email}")

# Register
provide("inventory", InventoryService())
provide("notification", NotificationService())

# Usage with @inject
@inject
def process_order(order_id: int, customer_email: str, inventory: InventoryService = None, notification: NotificationService = None):
    print(f"Processing order for {customer_email}")
    inventory.check(order_id)
    notification.send(customer_email)

process_order(1, "[email protected]")  # inventory and notification auto-injected
Solution

The solution is above. This mirrors how frameworks like injector and some DI libraries work under the hood.

Limitations of decorator-based DI:

  • Uses a global container (the service locator anti-pattern at the module level).
  • Type annotations are not used for resolution — only parameter names.
  • Hard to test without modifying global state.

Better production approach — use type annotations:

def inject(func: Callable) -> Callable:
hints = get_type_hints(func)

@wraps(func)
def wrapper(*args, **kwargs):
sig = inspect.signature(func)
params = list(sig.parameters.keys())
for param in params[len(args):]:
if param not in kwargs and param in hints:
cls = hints[param]
if cls in _TYPE_CONTAINER:
kwargs[param] = _TYPE_CONTAINER[cls]
return func(*args, **kwargs)

return wrapper
from typing import Callable, Any
import inspect

# Implement an @inject decorator that automatically resolves
# function parameters from a global container by parameter name.

_CONTAINER: dict[str, Any] = {}

def provide(name: str, instance: Any) -> None:
    _CONTAINER[name] = instance

def inject(func: Callable) -> Callable:
    """Decorator that injects arguments from _CONTAINER by parameter name."""
    pass
Expected Output
Processing order for [email protected]\nInventory checked\nNotification sent to [email protected]
Hints

Hint 1: Use inspect.signature(func).parameters to get the parameter names. For each parameter that is not provided by the caller, look it up in _CONTAINER by name.

Hint 2: The decorated function should accept positional args from the caller and fill in unset keyword args from the container.

#7FastAPI-Style Depends PatternMedium
fastapidependsdi-framework

Implement a minimal Depends system that mirrors FastAPI's dependency injection mechanism, including nested dependencies and test overrides.

Python
from typing import Callable, Any
import inspect
from functools import wraps

class Depends:
    def __init__(self, dependency: Callable):
        self.dependency = dependency

def resolve(handler: Callable, **overrides) -> Any:
    sig = inspect.signature(handler)
    kwargs = {}
    for name, param in sig.parameters.items():
        if name in overrides:
            kwargs[name] = overrides[name]
        elif isinstance(param.default, Depends):
            # Recursively resolve nested dependency
            kwargs[name] = resolve(param.default.dependency, **overrides)
        # Parameters with no default and not in overrides: skip (caller provides them)
    return handler(**kwargs)

# Dependency functions
def get_db():
    return "RealDB"

def get_cache():
    return "RealCache"

def get_current_user(db=Depends(get_db)):
    return f"user_from_{db}"

# Handler
def handle_request(
    user=Depends(get_current_user),
    db=Depends(get_db),
    cache=Depends(get_cache),
):
    return f"user={user}, db={db}, cache={cache}"

result = resolve(handle_request)
print(f"Request handled: {result}")

# Override for testing
result_test = resolve(handle_request, db="FakeDB", cache="FakeCache", user="testuser")
print(f"With overrides: {result_test}")
Solution

The solution is above. This is exactly how FastAPI's Depends() works internally, minus the async/await handling.

Real FastAPI usage:

async def get_db() -> AsyncSession:
async with SessionLocal() as session:
yield session

async def get_current_user(
token: str = Header(...),
db: AsyncSession = Depends(get_db)
) -> User:
return await verify_token(token, db)

@router.get("/profile")
async def get_profile(user: User = Depends(get_current_user)):
return {"email": user.email}

Test overrides in FastAPI:

def get_fake_db():
return FakeSession()

app.dependency_overrides[get_db] = get_fake_db

# Now all endpoints that Depends(get_db) receive FakeSession instead
from typing import Callable, Any
from dataclasses import dataclass

# Implement a minimal Depends system (like FastAPI's):
# class Depends:
#     def __init__(self, dependency: Callable)
#
# def resolve(handler: Callable, **overrides) -> Any:
#     Inspects handler's parameters, resolves Depends() annotations,
#     and calls the handler with all resolved deps.
#     overrides let tests provide fake deps.
Expected Output
Request handled: user=alice, db=FakeDB, cache=FakeCache\nWith overrides: user=testuser, db=FakeDB, cache=FakeCache
Hints

Hint 1: Inspect the handler signature. For each parameter with a Depends() default, call the wrapped dependency function recursively (dependencies can have their own Depends).

Hint 2: The overrides dict maps parameter names to replacement values — this is how you inject fakes in tests without modifying the handler.


Hard

#8Auto-Wiring ContainerHard
autowiringtype-hintsdi-container

Build an auto-wiring container that resolves class dependencies by inspecting __init__ type annotations recursively.

Python
from typing import get_type_hints, Type, TypeVar, Any
import inspect

T = TypeVar("T")

class AutoWireContainer:
    def __init__(self):
        self._registered: set[type] = set()
        self._singletons: dict[type, Any] = {}
        self._overrides: dict[type, Any] = {}

    def register(self, cls: type) -> None:
        self._registered.add(cls)

    def override(self, cls: type, instance: Any) -> None:
        self._overrides[cls] = instance

    def resolve(self, cls: type) -> Any:
        if cls in self._overrides:
            return self._overrides[cls]
        if cls in self._singletons:
            return self._singletons[cls]

        # Get __init__ parameter types (skip 'self' and 'return')
        try:
            hints = get_type_hints(cls.__init__)
        except Exception:
            hints = {}
        hints.pop("return", None)

        kwargs = {}
        for param_name, param_type in hints.items():
            kwargs[param_name] = self.resolve(param_type)

        instance = cls(**kwargs)
        self._singletons[cls] = instance
        return instance

# Services
class StockService:
    def update(self, item: str, qty: int) -> None:
        pass

class EmailService:
    def send(self, to: str, msg: str) -> None:
        pass

class OrderService:
    def __init__(self, stock: StockService, email: EmailService):
        self._stock = stock
        self._email = email

    def place(self, customer: str, item: str, qty: int) -> str:
        self._stock.update(item, qty)
        self._email.send(customer, f"Order placed for {item}")
        return "order placed"

# Auto-wire
container = AutoWireContainer()
container.register(StockService)
container.register(EmailService)
container.register(OrderService)

svc = container.resolve(OrderService)
print("OrderService resolved")
print(f"OrderService.stock is StockService: {isinstance(svc._stock, StockService)}")
print(f"OrderService.email is EmailService: {isinstance(svc._email, EmailService)}")
print(svc.place("[email protected]", "Widget", 1))
Solution

The solution is above. Auto-wiring reads type annotations and recursively resolves them — no manual factory registration needed.

Limitations:

  • Only works if __init__ parameters are type-annotated.
  • Cannot resolve Protocol types without explicit overrides.
  • Does not handle Optional, Union, or primitive types automatically.

Production libraries handle these edge cases: dependency-injector (most popular), lagom (type-based), punq (lightweight).

Override for testing:

fake_email = FakeEmailService()
container.override(EmailService, fake_email)
svc = container.resolve(OrderService)
# svc._email is now fake_email
from typing import get_type_hints, Type, TypeVar, Any
import inspect

T = TypeVar("T")

class AutoWireContainer:
    """Container that resolves dependencies by inspecting type annotations.
    register(cls) - register a class for autowiring
    resolve(cls) - resolve an instance, creating dependencies recursively
    """
    pass
Expected Output
OrderService resolved\nOrderService.stock is StockService: True\nOrderService.email is EmailService: True\norder placed
Hints

Hint 1: Use inspect.signature(cls.__init__) to get the constructor parameter types. For each annotated parameter, call resolve() recursively on that type.

Hint 2: Store resolved instances in a dict keyed by class to provide singleton behavior.

#9Circular Dependency DetectorHard
circular-dependencygraphdi-diagnostics

Build a cycle detector for dependency graphs using DFS. Use it to validate a DI container before startup.

Python
def find_cycles(graph: dict[str, list[str]]) -> list[list[str]]:
    visited = set()
    in_path = set()
    cycles = []

    def dfs(node: str, path: list[str]) -> None:
        if node in in_path:
            cycle_start = path.index(node)
            cycles.append(path[cycle_start:] + [node])
            return
        if node in visited:
            return

        visited.add(node)
        in_path.add(node)
        path.append(node)

        for dep in graph.get(node, []):
            dfs(dep, path)

        path.pop()
        in_path.discard(node)

    for node in graph:
        if node not in visited:
            dfs(node, [])

    return cycles

# Clean graph
clean = {
    "OrderService": ["StockService", "EmailService"],
    "StockService": ["DatabaseConnection"],
    "EmailService": [],
    "DatabaseConnection": [],
}
cycles = find_cycles(clean)
print("Clean graph: no cycles" if not cycles else f"Cycles: {cycles}")

# Cyclic graph
cyclic = {
    "A": ["B"],
    "B": ["C"],
    "C": ["A"],  # cycle!
    "D": ["A"],
}
cycles = find_cycles(cyclic)
print(f"Cyclic graph cycles: {cycles}")
Solution

The solution is above. Run this check at container startup:

class AutoWireContainer:
def validate(self) -> None:
graph = {}
for cls in self._registered:
hints = get_type_hints(cls.__init__)
hints.pop("return", None)
graph[cls.__name__] = [t.__name__ for t in hints.values()]
cycles = find_cycles(graph)
if cycles:
raise RuntimeError(f"Circular dependencies detected: {cycles}")

Real circular dependency example:

class AuthService:
def __init__(self, user_svc: "UserService"): ...

class UserService:
def __init__(self, auth_svc: AuthService): ... # circular!

Fix: Introduce an interface, use lazy resolution, or extract the shared logic into a third service that neither depends on the other.

from typing import Callable

# Given a dependency graph as a dict of name -> list of dependency names,
# write a function that detects circular dependencies and returns
# the cycles found (if any).

def find_cycles(graph: dict[str, list[str]]) -> list[list[str]]:
    """Return all cycles found in the dependency graph."""
    pass
Expected Output
Clean graph: no cycles\nCyclic graph cycles: [['A', 'B', 'C', 'A']]
Hints

Hint 1: Use DFS with a "currently visiting" set. When you encounter a node already in the current path, you have found a cycle.

Hint 2: Track the path as a list so you can reconstruct the cycle from the node where the cycle starts to where it was re-encountered.

#10Context-Scoped DI for Web RequestsHard
request-scopecontext-vardiweb

Implement request-scoped DI using contextvars.ContextVar so each simulated request gets its own scoped service instances.

Python
from contextvars import ContextVar
from typing import Callable, Any
import asyncio

_scope: ContextVar[dict] = ContextVar("_scope", default={})

class Config:
    """Singleton - shared across all requests."""
    value = "production"

class DbSession:
    """Scoped - one per request."""
    _counter = 0

    def __init__(self):
        DbSession._counter += 1
        self.session_id = DbSession._counter

_singletons: dict[type, Any] = {}
_scoped_factories: dict[type, Callable] = {}

def register_singleton(cls: type) -> None:
    _singletons[cls] = cls()

def register_scoped(cls: type, factory: Callable = None) -> None:
    _scoped_factories[cls] = factory or cls

def resolve(cls: type) -> Any:
    if cls in _singletons:
        return _singletons[cls]
    if cls in _scoped_factories:
        scope_cache = _scope.get()
        if cls not in scope_cache:
            scope_cache[cls] = _scoped_factories[cls]()
        return scope_cache[cls]
    raise KeyError(f"Not registered: {cls}")

async def handle_request(request_name: str) -> dict:
    # Each request sets its own scope cache
    _scope.set({})
    session_a = resolve(DbSession)
    session_b = resolve(DbSession)  # should be same instance within request
    config = resolve(Config)
    assert session_a is session_b, "Scoped: should be same within request"
    return {"request": request_name, "session_id": session_a.session_id, "config": config.value}

register_singleton(Config)
register_scoped(DbSession)

async def main():
    r1, r2 = await asyncio.gather(
        handle_request("Request 1"),
        handle_request("Request 2"),
    )
    print(f"Request 1: session_id differs from Request 2: {r1['session_id'] != r2['session_id']}")
    print(f"Request 1: config is same as Request 2: {r1['config'] == r2['config']}")

asyncio.run(main())
Solution

The solution is above. ContextVar is the asyncio-safe replacement for threading.local(). Each coroutine (request) gets its own copy of the scope cache — no shared state, no race conditions.

FastAPI's actual implementation (simplified):

from fastapi import Request

async def get_db(request: Request) -> AsyncSession:
if "db" not in request.state.__dict__:
request.state.db = SessionLocal()
try:
yield request.state.db
finally:
await request.state.db.close()

FastAPI stores scoped state in request.state, which is per-request by design.

from contextvars import ContextVar
from typing import Callable, Any
from dataclasses import dataclass, field

# Implement a request-scoped DI system using contextvars:
# - Each "request" runs in its own context
# - Scoped services are created once per context
# - Singleton services are shared across all contexts
# - Use threading or asyncio to simulate concurrent requests
Expected Output
Request 1: session_id differs from Request 2: True\nRequest 1: config is same as Request 2: True
Hints

Hint 1: Use ContextVar to store a dict of scoped instances. Each asyncio task gets its own copy of the ContextVar value.

Hint 2: Set the ContextVar at the start of each request with contextvars.copy_context().run() or by setting a new dict at request entry.

#11Full DI Container with Decorators, Scopes, and Auto-WiringHard
di-containerdecoratorsautowiringfull-system

Build a decorator-driven DI container with singleton/transient scopes, auto-wiring, test overrides, and dependency validation.

Python
from typing import get_type_hints, Type, TypeVar, Any
import inspect

T = TypeVar("T")

class RegistrationError(Exception):
    pass

class DIContainer:
    def __init__(self):
        self._singletons_reg: set[type] = set()
        self._transients_reg: set[type] = set()
        self._singleton_cache: dict[type, Any] = {}
        self._overrides: dict[type, Any] = {}

    def singleton(self, cls: type) -> type:
        self._singletons_reg.add(cls)
        return cls

    def transient(self, cls: type) -> type:
        self._transients_reg.add(cls)
        return cls

    def override(self, cls: type, instance: Any) -> None:
        self._overrides[cls] = instance

    def clear_overrides(self) -> None:
        self._overrides.clear()

    def _build(self, cls: type) -> Any:
        try:
            hints = get_type_hints(cls.__init__)
        except Exception:
            hints = {}
        hints.pop("return", None)

        kwargs = {}
        for param_name, param_type in hints.items():
            kwargs[param_name] = self.resolve(param_type)
        return cls(**kwargs)

    def resolve(self, cls: type) -> Any:
        if cls in self._overrides:
            return self._overrides[cls]
        if cls in self._singletons_reg:
            if cls not in self._singleton_cache:
                self._singleton_cache[cls] = self._build(cls)
            return self._singleton_cache[cls]
        if cls in self._transients_reg:
            return self._build(cls)
        raise RegistrationError(f"Type not registered: {cls.__name__}")

container = DIContainer()

# Register services
@container.singleton
class Config:
    db_url: str = "postgresql://localhost/app"

@container.singleton
class NotificationService:
    def send(self, msg: str) -> None:
        print(f"[Notification] {msg}")

@container.singleton
class OrderService:
    def __init__(self, config: Config, notifier: NotificationService):
        self._config = config
        self._notifier = notifier

    def place(self, item: str) -> None:
        self._notifier.send(f"Order placed: {item}")

# Test it
svc = container.resolve(OrderService)
print("OrderService wired successfully")
svc.place("Widget")

# Override for testing
class FakeNotifier:
    def __init__(self): self.last_msg = None
    def send(self, msg: str) -> None:
        self.last_msg = msg
        print(f"[FakeNotifier] {msg}")

fake = FakeNotifier()
container.override(NotificationService, fake)
container._singleton_cache.clear()  # reset to re-wire
svc2 = container.resolve(OrderService)
svc2.place("Gadget")
print(f"With override: notification was {type(svc2._notifier).__name__}")
container.clear_overrides()

# Validation
class Unregistered:
    pass

@container.transient
class BrokenService:
    def __init__(self, dep: Unregistered):
        pass

try:
    container.resolve(BrokenService)
except RegistrationError as e:
    print(f"Validation: RegistrationError caught for missing dep")
Solution

The solution is above. This container is production-usable for medium-sized applications. It demonstrates all the DI patterns in one coherent implementation:

  1. Decorator registration (@container.singleton, @container.transient) — clean, visible at the class definition.
  2. Auto-wiring — reads __init__ type hints, no manual factory functions.
  3. Singleton cache — one instance per type across the app lifetime.
  4. Test overrides — inject fakes without modifying any production code.
  5. Error on missing registration — fail fast with a clear message at resolve time, not at call time.

Production improvement: Add @container.interface(INotificationPort, NotificationService) to support resolving by protocol rather than concrete class.

# Build a production-quality DI container that supports:
# @container.singleton  - decorator to register a class as singleton
# @container.transient  - decorator to register a class as transient
# container.resolve(cls) - auto-wire from type annotations
# container.override(cls, instance) - test override
# Validate: detect unregistered dependencies at resolve time
Expected Output
OrderService wired successfully\nOrder placed\nWith override: notification was FakeNotifier\nValidation: RegistrationError caught for missing dep
Hints

Hint 1: Use class decorators to add classes to the container registry. The decorator returns the class unchanged but registers it internally.

Hint 2: At resolve() time, walk the __init__ type hints, resolve each recursively, check for overrides, and raise a clear error for unregistered types.

© 2026 EngineersOfAI. All rights reserved.