waku¶
waku [枠 or わく] means framework in Japanese.
waku is a modular, type-safe Python framework for scalable, maintainable applications. Inspired by NestJS, powered by Dishka IoC.
[!WARNING]
waku
is going through a major rewrite, so docs aren't fully up-to-date yet. Stick to this README and our examples for now.For more details, check out our
waku
deepwiki page.
Why waku
?¶
- 🧩 Modular architecture: Group related code with explicit imports/exports for clear boundaries and responsibilities.
- 💉 First-class Dependency Injection: Built on Dishka with flexible provider patterns (singleton, scoped, transient); swap implementations easily.
- 📨 Event-driven & CQRS: Handle commands, queries, and events with a comprehensive CQRS implementation, pipeline chains, and centralized processing inspired by MediatR (C#).
- 🔌 Framework-agnostic & Integrations: Works with FastAPI, Litestar, FastStream, Aiogram, and more - no vendor lock-in.
- 🧰 Extensions & Lifecycle Hooks: Hook into the app lifecycle for logging, validation, and custom logic; precise startup/shutdown management.
- 🛡️ Production-ready: Type-safe APIs, robust validation, and scalable testing support.
Who is it for?¶
- 👥 Enterprise development teams building modular, maintainable backend services or microservices
- 🏗️ Architects and tech leads seeking a structured framework with clear dependency boundaries and testability
- 🐍 Python developers frustrated with monolithic codebases and looking for better separation of concerns
- 🌏 Engineers from other ecosystems (Java Spring, C# ASP.NET, TypeScript NestJS) wanting familiar patterns in Python
- 📈 Projects requiring scalability both in codebase organization and team collaboration
Quick Start¶
Installation¶
Understanding the Basics¶
Waku is built around a few core concepts:
- 🧩 Modules: Classes decorated with
@module()
that define boundaries for application components and establish clear dependency relationships. - 🧑🔧 Providers: Injectable services and logic registered within modules.
- 💉 Dependency Injection: Type-safe, flexible wiring powered by Dishka IoC container.
- 🏭 WakuFactory: The entry point that creates a
WakuApplication
instance from your root module. - 🔄 Application Lifecycle: Initialization and shutdown phases, enhanced with extension hooks.
This structure keeps your code clean and your dependencies explicit.
waku
is framework-agnostic - entrypoints (such as HTTP handlers) are provided by integrations, not the core.
Basic Example¶
Here's a minimal example showing the core concepts:
import asyncio
from waku import WakuFactory, module
from waku.di import scoped
# 1. Define a provider (service)
class GreetingService:
async def greet(self, name: str) -> str:
return f'Hello, {name}!'
# 2. Create a module and register the provider
@module(providers=[scoped(GreetingService)])
class GreetingModule:
pass
# 3. Create a root module that imports other modules
@module(imports=[GreetingModule])
class AppModule:
pass
async def main() -> None:
# 4. Bootstrap the application with WakuFactory
app = WakuFactory(AppModule).create()
# 5. Use the application with a properly scoped container
async with app, app.container() as c:
# 6. Resolve and use dependencies
svc = await c.get(GreetingService)
print(await svc.greet('waku'))
if __name__ == '__main__':
asyncio.run(main())
More Realistic Example¶
Let's add protocols and module exports:
import asyncio
from typing import Protocol
from waku import WakuFactory, module
from waku.di import scoped, singleton
# Define a protocol for loose coupling
class Logger(Protocol):
async def log(self, message: str) -> None: ...
# Implementation of the logger
class ConsoleLogger:
async def log(self, message: str) -> None:
print(f'[LOG] {message}')
# Service that depends on the logger
class UserService:
def __init__(self, logger: Logger) -> None:
self.logger = logger
async def create_user(self, username: str) -> str:
user_id = f'user_{username}'
await self.logger.log(f'Created user: {username}')
return user_id
# Infrastructure module provides core services
@module(
providers=[singleton(ConsoleLogger, provided_type=Logger)],
exports=[Logger], # Export to make available to other modules
)
class InfrastructureModule:
pass
# Feature module for user management
@module(
imports=[InfrastructureModule], # Import dependencies from other modules
providers=[scoped(UserService)],
)
class UserModule:
pass
# Application root module
@module(imports=[UserModule])
class AppModule:
pass
async def main() -> None:
app = WakuFactory(AppModule).create()
async with app, app.container() as c:
user_service = await c.get(UserService)
user_id = await user_service.create_user('alice')
print(f'Created user with ID: {user_id}')
if __name__ == '__main__':
asyncio.run(main())
Next Steps¶
Want to learn more? Here's where to go next:
- Get familiar with module exports and imports
- Try different provider scopes
- Add CQRS for clean command handling
- Use extension hooks to customize your app
- Connect with your favorite framework
Check our Getting Started guide and browse the examples directory for inspiration.
Documentation¶
Contributing¶
Top contributors¶
Roadmap¶
- Create logo
- Improve inner architecture
- Improve documentation
- Add new and improve existing validation rules
- Provide example projects for common architectures
Support¶
License¶
This project is licensed under the terms of the MIT License.
Acknowledgements¶
- Dishka – Dependency Injection framework powering
waku
IoC container. - NestJS – Primary inspiration for modular architecture, design patterns and some implementation details.
- MediatR (C#) – Inspiration and implementation details for the CQRS subsystem.