Quick Start¶
Let's build your first Haiway application! This guide will walk you through creating a simple user management system that demonstrates Haiway's core features.
Your First State¶
Haiway applications are built around immutable state objects that serve as data and dependency containers:
What's happening here:
State
is Haiway's base using dataclass like definitions that automatically makes objects immutable- Fields are defined with type hints - Haiway validates types at runtime
- Once created,
User
objects cannot be modified (attemptinguser.name = "new"
raises an error) - Optional fields use union types (
str | None
) with default values
Working with Context¶
Haiway uses context scopes to manage state and enable dependency injection:
from haiway import ctx
import asyncio
async def main():
# Create immutable user object
alice = User(
id="1",
name="Alice",
email="alice@example.com",
)
# Create a copy with updated fields (original unchanged)
other_alice: User = alice.updated(
id="2",
email="other_alice@example.com",
)
# Create context scope and inject state
async with ctx.scope("app", alice):
# Access state from anywhere within this scope
# Uses the type as a key to retrieve the correct state
current_alice: User = ctx.state(User)
print(f"Current user: {current_alice.name}")
if __name__ == "__main__":
asyncio.run(main())
What's happening here:
ctx.scope("app", alice)
creates an execution context named "app", containing thealice
state- State is automatically propagated to all code within the scope (including nested function calls)
ctx.state(User)
retrieves contextual state using its type as a key- The
.updated()
method creates a copy of object with modified fields, leaving the original unchanged - Context automatically manages the lifecycle - when the scope exits, resources are cleaned up
Dependency Injection¶
Haiway implements dependency injection through function protocols and state containers:
from typing import Protocol, runtime_checkable, Sequence
from haiway import statemethod
# Function interface - single __call__ method only
@runtime_checkable
class UsersFetching(Protocol):
async def __call__(self) -> Sequence[User]: ...
class UsersService(State):
fetching: UsersFetching
@statemethod
async def fetch_users(self) -> Sequence[User]:
return await self.fetching()
# Factory function for service implementation
def InMemoryUsersService() -> UsersService:
# Implementation function
async def in_memory_users_fetching() -> Sequence[User]:
# In a real implementation, this would access configuration
# or stored data from context state
return (
User(id="1", name="Alice", email="alice@example.com"),
User(id="2", name="Bob", email="bob@example.com"),
)
return UsersService(fetching=in_memory_users_fetching)
async def main():
# Create service with implementation
service = InMemoryUsersService()
# Use in context scope
async with ctx.scope("app", service):
# Access functionality through class methods
users: Sequence[User] = await UsersService.fetch_users()
print(f"Found {len(users)} users")
if __name__ == "__main__":
asyncio.run(main())
What's happening here:
- Protocol Interface:
UsersFetching
defines a contract with a single__call__
method - this ensures implementations are interchangeable - Service Container:
UsersService
holds function implementations and provides a clean API through class methods - Implementation Function:
in_memory_users_fetching
is the concrete implementation that returns actual data - Factory Pattern:
InMemoryUsersService()
creates a configured service instance with the implementation wired up - Context Injection: The service is injected into the context scope, making it available throughout the execution
- Transparent Access:
UsersService.fetch_users()
internally retrieves the service from context and calls the implementation - Type Safety:
@runtime_checkable
enables runtime validation that implementations match the protocol
This pattern allows you to easily swap implementations (in-memory, database, API) without changing the calling code.
Key Concepts¶
- Immutable State: All state objects are immutable by default
- Type Safety: Full type checking support with modern Python features
- Context Management: Scoped execution with state propagation
- Dependency Injection: Clean separation of concerns using function based state interfaces
Disposables¶
Disposables are resources that require automatic clean up after they are used. You can put them in the context and they are going to be initialized when this context starts and cleaned up once the context is finished. You can define disposables like this:
from contextlib import asynccontextmanager
@asynccontextmanager
async def database_connection():
print("Opening database connection")
try:
yield DatabaseState(connection={"status": "connected"})
finally:
print("Closing database connection")
Here the database_connection()
function is a factory for a dependency that needs cleanup after
it's used. It gets called once on the context beginning and the second time when the context ends.
Advanced Context Usage¶
Using Context Presets¶
For more advanced scenarios, you can use context presets to package state and disposables together:
from haiway.context import ContextPreset
# Create a preset with predefined state
api_preset = ContextPreset(
name="api_client",
state=[
UsersService(fetching=production_users_fetching),
ApiConfig(base_url="https://api.example.com", timeout=60)
]
)
async def main():
# Use preset directly - no need for preset registry
async with ctx.scope(api_preset):
users = await UsersService.fetch_users()
config = ctx.state(ApiConfig)
print(f"Using {config.base_url} with {len(users)} users")
# Override preset state with explicit values
async with ctx.scope(api_preset, ApiConfig(timeout=30)):
config = ctx.state(ApiConfig)
print(f"Timeout overridden to {config.timeout}")
if __name__ == "__main__":
asyncio.run(main())
What's happening here:
- Preset Definition:
ContextPreset
packages multiple state objects together - Direct Usage: Pass the preset directly to
ctx.scope()
instead of a string name - State Override: Explicit state parameters override preset state by type
- Priority System: Explicit state (highest) > disposables > preset state > contextual state (lowest)
What's Next?¶
- Explore the Functionalities
- Learn about State
- See how to structure Packages