Dependency Injection
myfy's DI system provides compile-time resolution, scoped lifetimes, and zero reflection on hot paths.
Why DI?
Dependency Injection solves three core problems:
- Testability - Easy to mock dependencies
- Flexibility - Swap implementations without changing code
- Decoupling - Components don't create their dependencies
Basic Usage
Register a Provider
from myfy.core import provider, SINGLETON
@provider(scope=SINGLETON)
def database(settings: Settings) -> Database:
return Database(settings.db_url)
Inject Dependencies
@route.get("/users")
async def list_users(db: Database) -> list[User]:
return await db.get_all_users()
That's it! myfy automatically:
1. Detects the db: Database dependency
2. Looks up the provider for Database
3. Calls the provider with its dependencies
4. Injects the result into your handler
Scopes
myfy provides three scopes for different lifetimes:
SINGLETON Scope
One instance per application. Perfect for: - Database connection pools - Configuration objects - Caches - HTTP clients
REQUEST Scope
One instance per HTTP request. Perfect for: - Database sessions/transactions - Request-specific context - Per-request caches
TASK Scope
One instance per background task. Perfect for: - Task-specific resources - Isolated task context
How It Works
Compile-Time Resolution
myfy analyzes dependencies at startup, not at request time:
# Startup (once):
# 1. Parse type hints from handlers
# 2. Build dependency graph
# 3. Detect cycles
# 4. Create injection plans
# 5. Cache plans in O(1) lookup table
# Request time (fast):
# 1. Lookup injection plan (O(1))
# 2. Execute plan (simple function calls)
# 3. Call handler
Result: Zero reflection overhead on hot path.
Dependency Graph
@provider(scope=SINGLETON)
def database(settings: Settings) -> Database:
return Database(settings.db_url)
@provider(scope=REQUEST)
def session(db: Database) -> Session:
return db.create_session()
@provider(scope=SINGLETON)
def user_repository(session: Session) -> UserRepository:
return UserRepository(session)
@route.get("/users")
async def list_users(repo: UserRepository) -> list[User]:
return await repo.list_all()
Dependency graph:
Settings (built-in)
↓
Database (SINGLETON)
↓
Session (REQUEST)
↓
UserRepository (SINGLETON)
↓
list_users (handler)
Advanced Patterns
Factory Functions
@provider(scope=SINGLETON)
def create_database(settings: Settings) -> Database:
if settings.db_type == "postgres":
return PostgresDatabase(settings.postgres_url)
elif settings.db_type == "mysql":
return MySQLDatabase(settings.mysql_url)
else:
raise ValueError(f"Unknown db_type: {settings.db_type}")
Constructor Injection
class UserService:
def __init__(self, repo: UserRepository, cache: Cache):
self.repo = repo
self.cache = cache
async def get_user(self, user_id: int) -> User:
# Check cache
cached = await self.cache.get(f"user:{user_id}")
if cached:
return User.model_validate(cached)
# Get from database
user = await self.repo.get(user_id)
if user:
await self.cache.set(f"user:{user_id}", user.model_dump())
return user
@provider(scope=SINGLETON)
def user_service(repo: UserRepository, cache: Cache) -> UserService:
return UserService(repo, cache)
Multiple Implementations
# Abstract interface
class Storage(Protocol):
async def save(self, key: str, value: Any) -> None: ...
async def load(self, key: str) -> Any: ...
# Implementations
class RedisStorage:
async def save(self, key: str, value: Any) -> None:
await redis.set(key, value)
class S3Storage:
async def save(self, key: str, value: Any) -> None:
await s3.put_object(key, value)
# Register based on config
@provider(scope=SINGLETON)
def storage(settings: Settings) -> Storage:
if settings.storage_type == "redis":
return RedisStorage(settings.redis_url)
elif settings.storage_type == "s3":
return S3Storage(settings.s3_bucket)
else:
raise ValueError(f"Unknown storage_type: {settings.storage_type}")
Request Scope Deep Dive
How Request Scope Works
Request-scoped dependencies use Python's contextvars:
from contextvars import ContextVar
_request_bag: ContextVar[dict[str, Any]] = ContextVar("request_bag")
# At request start:
_request_bag.set({})
# When resolving REQUEST-scoped dependency:
bag = _request_bag.get()
if "session" not in bag:
bag["session"] = create_session()
return bag["session"]
# At request end:
_request_bag.set({}) # Clear the bag
Lifecycle
HTTP Request arrives
↓
Create request context (contextvar)
↓
Resolve REQUEST-scoped dependencies
↓
Execute handler
↓
Clear request context
↓
Response sent
Example: Database Session
@provider(scope=REQUEST)
def session(engine: Engine) -> AsyncSession:
"""Create a new database session for each request."""
return AsyncSession(engine)
@route.post("/users")
async def create_user(
body: CreateUserDTO,
session: AsyncSession
) -> User:
# Same session instance throughout the request
user = User(**body.model_dump())
session.add(user)
await session.commit()
return user
Scope Validation
myfy validates scopes at compile-time:
Valid: Singleton → Singleton
@provider(scope=SINGLETON)
def cache() -> Cache:
return Cache()
@provider(scope=SINGLETON)
def service(cache: Cache) -> Service: # ✅ OK
return Service(cache)
Invalid: Singleton → Request
@provider(scope=REQUEST)
def session() -> Session:
return Session()
@provider(scope=SINGLETON)
def service(session: Session) -> Service: # ❌ ERROR
# Singleton can't depend on REQUEST scope!
return Service(session)
Why? A singleton lives for the entire application, but a request-scoped dependency is recreated per request. This would be a lifetime mismatch.
Scope Hierarchy
Rule: Dependencies can only inject from equal or longer lifetimes.
Cycle Detection
myfy detects circular dependencies at startup:
@provider(scope=SINGLETON)
def service_a(b: ServiceB) -> ServiceA:
return ServiceA(b)
@provider(scope=SINGLETON)
def service_b(a: ServiceA) -> ServiceB:
return ServiceB(a)
# At startup:
# Error: Circular dependency detected:
# ServiceA → ServiceB → ServiceA
Testing with DI
Override Dependencies
import pytest
from myfy.core import Application, provider, SINGLETON
@pytest.fixture
def app():
app = Application(settings_class=TestSettings, auto_discover=False)
# Override database with mock
@provider(scope=SINGLETON)
def mock_database() -> Database:
return MockDatabase()
app.initialize()
return app
def test_user_creation(app):
service = app.container.get(UserService)
user = await service.create_user("[email protected]")
assert user.email == "[email protected]"
Mock Dependencies
from unittest.mock import Mock
@pytest.fixture
def mock_cache():
cache = Mock(spec=Cache)
cache.get.return_value = None
cache.set.return_value = None
return cache
@pytest.fixture
def user_service(mock_cache):
repo = InMemoryUserRepository()
return UserService(repo, mock_cache)
def test_user_service(user_service, mock_cache):
user = await user_service.get_user(1)
mock_cache.get.assert_called_once_with("user:1")
Performance
Benchmark: DI Resolution
myfy's DI adds minimal overhead because: 1. Type hints parsed once at startup 2. Injection plans cached in dict (O(1)) 3. No reflection during requests 4. Simple function calls only
Common Patterns
Service Layer
@provider(scope=SINGLETON)
def user_service(
repo: UserRepository,
cache: Cache,
email: EmailService
) -> UserService:
return UserService(repo, cache, email)
@route.post("/users")
async def create_user(
body: CreateUserDTO,
service: UserService
) -> User:
return await service.create_user(body)
Unit of Work
@provider(scope=REQUEST)
def unit_of_work(session: AsyncSession) -> UnitOfWork:
return UnitOfWork(session)
@route.post("/orders")
async def create_order(
body: CreateOrderDTO,
uow: UnitOfWork
) -> Order:
order = await uow.orders.create(body)
await uow.commit()
return order
Repository Pattern
@provider(scope=SINGLETON)
def user_repository(session_factory: SessionFactory) -> UserRepository:
return SQLAlchemyUserRepository(session_factory)
@route.get("/users/{user_id}")
async def get_user(
user_id: int,
repo: UserRepository
) -> User:
return await repo.get(user_id)
Best Practices
✅ DO
- Use
SINGLETONfor stateless services - Use
REQUESTfor database sessions - Keep providers simple and focused
- Inject interfaces, not implementations
- Test by mocking dependencies
❌ DON'T
- Don't inject REQUEST scope into SINGLETON
- Don't store mutable state in SINGLETON
- Don't create dependencies manually
- Don't use global variables
- Don't bypass the container
Troubleshooting
Missing Provider
Solution: Register a provider:
@provider(scope=SINGLETON)
def database(settings: Settings) -> Database:
return Database(settings.db_url)
Circular Dependency
Solution: Refactor to remove the cycle: - Extract shared logic into a third service - Use callbacks or events instead of direct injection
Scope Mismatch
Solution: Change the dependent scope or use a factory:
# Instead of:
@provider(scope=SINGLETON)
def service(session: Session) -> Service: # ❌
return Service(session)
# Use:
@provider(scope=REQUEST)
def service(session: Session) -> Service: # ✅
return Service(session)
Next Steps
- Modules - Organize your application
- Configuration - Type-safe settings
- Testing Guide - Test with DI
- Building Modules - Create reusable modules