Recipes#

Real-world, copy-pasteable patterns that combine several decorators. Every snippet on this page is executed as-is by the test suite, so it runs exactly as shown. For a single-decorator tour see Quick start; for the standalone runnable scripts see Examples.

Immutable value object#

Use @Value for small, comparable, hashable values that should never change after construction — money, coordinates, identifiers. It is immutable on its own, with no @dataclass(frozen=True) needed.

from dataclasses import FrozenInstanceError
from inito import Value


@Value
class Money:
    amount: int
    currency: str = "USD"


price = Money(500)
print(price)                       # Money(amount=500, currency='USD')
print(price == Money(500, "USD"))  # True
print(price.get_currency())        # USD  (getters, but never setters)

try:
    price.amount = 0               # immutable
except FrozenInstanceError:
    print("cannot mutate a @Value")

# Hashable, so it works as a dict key or set member:
totals = {Money(500): "five dollars"}

Configuration object with defaults and a mutable default#

Required fields first, defaulted fields after — the same ordering Python uses. For a mutable default (a list, dict, set), stack @dataclass so inito picks up field(default_factory=...); each instance then gets its own fresh container.

from dataclasses import dataclass, field
from inito import Data


@Data
@dataclass
class ServerConfig:
    host: str = "localhost"
    port: int = 8080
    tags: list = field(default_factory=list)


config = ServerConfig(port=9000)
print(config)                      # ServerConfig(host='localhost', port=9000, tags=[])
ServerConfig().tags.append("a")
print(ServerConfig().tags)         # []  — a fresh list per instance, not shared

Fluent builder for a request/response model#

@Builder shines when a type has several optional fields and you want readable, order-independent construction. to_builder=True also adds a .to_builder() for deriving a modified copy.

from dataclasses import dataclass
from inito import Builder


@Builder(to_builder=True)
@dataclass
class HttpRequest:
    method: str
    url: str
    timeout: float = 30.0
    retries: int = 0


request = (
    HttpRequest.builder()
    .method("GET")
    .url("/users")
    .timeout(5.0)
    .build()
)
print(request.timeout)             # 5.0

# Derive a variant without mutating the original:
with_retries = request.to_builder().retries(3).build()
print(with_retries.retries, request.retries)   # 3 0

Service layer with dependency injection#

@Singleton/@Service register classes; @Inject fills a function’s annotated parameters from the container. A parameter is autowired only if its type is a registered service — plain values (page_size: int) keep their own defaults. Combine with @RequiredArgsConstructor so a field annotation is all you need, no hand-written __init__.

from inito import Service, Singleton, Inject, RequiredArgsConstructor, default_container


@Singleton
class Database:
    def __init__(self) -> None:
        self.rows = {1: "Ada", 2: "Linus"}


@Service
@RequiredArgsConstructor
class UserRepository:
    db: Database                    # autowired

    def name(self, user_id: int) -> str:
        return self.db.rows[user_id]


@Service
@RequiredArgsConstructor
class Greeter:
    repo: UserRepository            # autowired, transitively

    def greet(self, user_id: int) -> str:
        return f"Hello, {self.repo.name(user_id)}"


@Inject
def handle(greeter: Greeter) -> None:
    print(greeter.greet(1))         # Hello, Ada


handle()

# The same graph, resolved explicitly:
print(default_container.get(Greeter).greet(2))   # Hello, Linus

# Every @Service class is still an ordinary class you can build by hand:
manual = Greeter(UserRepository(Database()))

Transient scope and an isolated container#

Singletons are cached; Scope.TRANSIENT rebuilds on every resolution. Use a dedicated Container (instead of the shared default_container) to keep a subsystem’s registrations isolated — handy in tests.

from inito import Service, Scope
from inito.di import Container


container = Container()


@Service(scope=Scope.TRANSIENT, container=container)
class RequestContext:
    def __init__(self) -> None:
        self.token = object()


a = container.get(RequestContext)
b = container.get(RequestContext)
print(a is b)                      # False — a fresh instance each time
print(container.is_registered(RequestContext))   # True

Compose exactly the capabilities you want#

@Data is a bundle; when you want less, stack the atomic decorators. This class gets a repr, value equality, and read-only accessors — but no constructor and no setters.

from inito import ToString, EqualsAndHashCode, Getter


@ToString
@EqualsAndHashCode
@Getter
class Point:
    x: int
    y: int


p = Point()
p.x, p.y = 1, 2
print(p)                # Point(x=1, y=2)
print(p.get_x())        # 1
print(p == p)           # True

Inheritance#

Fields accumulate across the MRO, base-class fields first — so a subclass constructor takes the base’s fields followed by its own.

from inito import Data


@Data
class Animal:
    name: str


@Data
class Dog(Animal):
    breed: str


rex = Dog("Rex", "Labrador")
print(rex)              # Dog(name='Rex', breed='Labrador')
print(rex.name, rex.breed)   # Rex Labrador

Add your own methods#

inito only attaches the members each decorator owns; anything else you write is left untouched, so decorated classes are just normal classes.

from inito import Data


@Data
class Temperature:
    celsius: float

    def to_fahrenheit(self) -> float:
        return self.celsius * 9 / 5 + 32


t = Temperature(20)
print(t.to_fahrenheit())    # 68.0
print(t)                    # Temperature(celsius=20)