Getting Started
A quick tour of installing Inlay and assembling your first dependency context. If you haven't yet, the introduction explains what dependency contexts are and why Inlay represents them as Protocol types.
Install
Inlay requires Python 3.14 or newer.
pip install inlay
A minimal example
Declare what your code needs as a Protocol, register concrete constructors, and let Inlay assemble an implementation:
from typing import Protocol
from inlay import RegistryBuilder, compiled
class Database:
def __init__(self, url: str) -> None:
self.url = url
class UserService:
def __init__(self, db: Database) -> None:
self.db = db
class AppContext(Protocol):
@property
def users(self) -> UserService: ...
registry = (
RegistryBuilder()
.register(Database)(Database)
.register(UserService)(UserService)
)
@compiled(registry)
def make_app(url: str) -> AppContext: ...
app = make_app(url='postgres://localhost/app')
assert app.users.db.url == 'postgres://localhost/app'
A few things to notice:
make_apphas no body. The@compileddecorator inspects its signature, solves the dependency graph againstregistry, and replaces it with a generated implementation.urlis a runtime parameter. It flows from the caller intoDatabase.__init__because the solver matched the parameter name and type to a constructor argument it could not satisfy from the registry alone.- The resolution happens once, at module import time. If anything is unsatisfiable,
make_appfails to compile, so the program never starts in a partially-wired state.
Hierarchical contexts
In real applications, some dependencies only exist after a runtime event — a request arrives, a user authenticates, a transaction begins. Imagine a UserRepository that scopes database access to a specific user:
class UserRepository:
def __init__(self, db: Database, user_id: str) -> None:
self.db = db
self.user_id = user_id
Database is fine to wire up at startup, but user_id is per-request — there's no sensible value to register globally. We need a way to express "UserRepository is only constructible after a user authenticates."
Inlay models this with a transition: a method on the parent context that returns a child context with extra fields in scope. Let's build it up.
Declare the authorized scope
First, declare what becomes reachable once a user is authenticated:
class AuthorizedContext(Protocol):
@property
def repo(self) -> UserRepository: ...
Nothing here mentions user_id — that's an implementation detail of UserRepository. The child context only declares its user-facing surface.
Add the transition to the parent
Next, advertise the entry point on the root context:
class AppContext(Protocol):
def authorize(self, token: str) -> AuthorizedContext: ...
This is still just a Protocol. We haven't told Inlay how authorize works — only that AppContext is anything with a method matching this signature.
Implement the transition
The implementation is just a function that returns the new fields the child scope gains over the parent. A TypedDict is a clean way to express those fields:
from typing import TypedDict
class AuthorizedFields(TypedDict):
user_id: str
def authorize(token: str) -> AuthorizedFields:
# validate token, look up user, etc.
return {'user_id': 'u-123'}
When this function runs, its return value contributes a user_id: str into the child context's resolution scope. The child inherits everything from the parent (so Database is still available) and adds these new fields on top.
Wire it up
register_method binds the function to the protocol method:
registry = (
RegistryBuilder()
.register(Database)(Database)
.register(UserRepository)(UserRepository)
.register_method(AppContext, AppContext.authorize)(authorize)
)
Inlay can now resolve AuthorizedContext.repo: inside the authorized scope it has Database (inherited from the parent) and user_id (introduced by the transition's return type), which is everything UserRepository.__init__ requires.
Compile and call it
The final step is the same as before — compile the root and walk through the transition:
@compiled(registry)
def make_app(url: str) -> AppContext: ...
app = make_app(url='postgres://localhost/app')
authorized = app.authorize(token='...')
assert authorized.repo.user_id == 'u-123'
UserRepository is constructed only once authorize(...) is called — up to that point no user_id exists, and Inlay never attempts to build it. The same pattern composes recursively: child contexts can declare their own transitions.
Where to go next
- How does it work — the
compile()model and what kinds of targets it supports. - The
gems-webexample — a full Starlette application showing modular registries, qualifiers, async transitions, and pluggable backends.