Using InitO with your framework#
InitO has zero runtime dependencies and generates plain methods on plain classes, so it drops into any Python project — Django, FastAPI, Sanic, Flask, Litestar, or no framework at all. This page covers the two things worth knowing when you combine it with a framework: which decorators to reach for on framework model classes, and how the dependency-injection layer behaves in async request handlers.
The one rule: constructors vs. additive decorators#
InitO’s decorators fall into two groups:
Additive —
@Getter,@Setter,@ToString,@EqualsAndHashCode. These only add methods and never touch construction. They compose freely with any class, including framework models.Constructor-owning —
@Data,@Value,@NoArgsConstructor,@AllArgsConstructor,@RequiredArgsConstructor, and@Builder’s defaultbuild(). These generate (or bypass)__init__.
Frameworks like Pydantic, SQLAlchemy, and Django run important logic
in their own __init__ (validation, ORM instrumentation, field descriptors). So
on a framework’s model class, prefer the additive decorators, and let the
framework own construction.
from inito import Getter, ToString
from pydantic import BaseModel
@Getter
@ToString
class Settings(BaseModel):
host: str = "localhost"
port: int = 5432
s = Settings()
s.get_host() # "localhost" — inito accessor
repr(s) # "Settings(host='localhost', port=5432)" — inito __repr__
For your own domain / DTO / service objects — the classes that aren’t a
framework base class — use the full set (@Data, @Value, @Builder) exactly
as you would anywhere.
Pydantic v2 is auto-detected#
InitO recognizes a Pydantic v2 model (by duck-typing — it never imports Pydantic, so the zero-dependency promise holds) and adapts so the right thing happens automatically:
@Builderjust works — bare@Builderon a Pydantic model constructs through Pydantic’s validating__init__(no need to passuse_init=True) and reads each field’s default and required-ness from the model, so a Pydantic-defaulted field is optional in the builder rather than wrongly required:from inito import Builder from pydantic import BaseModel @Builder class User(BaseModel): name: str age: int = 0 # Pydantic default -> optional in the builder user = User.builder().name("Ada").build() # age -> Pydantic's default (0) user.__pydantic_fields_set__ # {"name"} — a fully-valid model User.builder().name("Ada").age("nope").build() # raises pydantic.ValidationError
Constructor-owning decorators are rejected — applying
@Data,@Value,@AllArgsConstructor,@NoArgsConstructor, or@RequiredArgsConstructorto a Pydantic model would overwrite Pydantic’s validating__init__and silently disable validation, so InitO raisesDecoratorConfigurationErrorat decoration time and points you at@Builderplus the additive decorators.Additive decorators (
@Getter/@Setter/@ToString/@EqualsAndHashCode) compose freely, as shown above.
Builders that respect a framework constructor: use_init=True#
For a Pydantic model this is automatic (above). For SQLAlchemy, Django,
or any hand-written __init__, opt in explicitly with use_init=True.
By default @Builder’s build() assigns fields directly and bypasses
__init__ — this is what keeps it fast and lets it work with InitO’s own
immutable classes. On a class whose constructor does real work that means it
would be skipped. Pass use_init=True and build() instead constructs
through the class’s own __init__, so that logic runs:
from inito import Builder
@Builder(use_init=True)
class Temperature:
celsius: float
def __init__(self, celsius: float) -> None:
self.celsius = round(celsius, 2) # constructor logic must run
Temperature.builder().celsius(3.14159).build().celsius # 3.14
In use_init=True mode the builder is a keyword-argument accumulator: only the
fields you actually set are passed to the constructor, so the constructor’s
own defaults and required-argument errors apply — not InitO’s. It works the
same way with SQLAlchemy 2.0 declarative models and any hand-written __init__:
from inito import Builder
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
@Builder(use_init=True)
class Widget(Base):
__tablename__ = "widget"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column()
widget = Widget.builder().id(1).name("gadget").build() # fully instrumented ORM object
Dependency injection in async handlers (FastAPI / Sanic / Starlette)#
The DI container is safe to resolve from async code. container.get() and
@Inject are synchronous, do no I/O, and singleton construction is guarded by
double-checked locking, so concurrent resolution from many coroutines yields a
single shared instance with no deadlock.
@Inject wraps an async def transparently — it fills the container-known
parameters and returns the coroutine unchanged:
from inito import Inject, RequiredArgsConstructor, Service, Singleton
@Singleton
class Repository:
users = {"ada": 30} # seed data — no constructor needed
@Service
@RequiredArgsConstructor # inito writes __init__(self, repo)
class UserService:
repo: Repository
# FastAPI example — the handler's dependencies are wired by InitO, not the route.
@Inject
async def get_age(name: str, service: UserService) -> int:
return service.repo.users[name]
Register services at module import time (the decorator does this for you). Because DI resolves constructor type hints at import, define your services at module scope — a service class defined inside a function can’t have its annotations resolved against the module globals.
For a heavier request-scoped lifecycle, keep using your framework’s own
dependency system (FastAPI Depends, etc.) and let InitO wire the singletons
and services behind it.
A note on __slots__ and performance#
InitO’s generated constructors keep CPython’s key-sharing instance dict intact, so attribute access on decorated objects stays at handwritten speed — see Performance. This holds inside web frameworks too; there is no per-request or per-instance reflection cost, because all reflection happens once at import.