Lesson 8 — use dependency injection¶
What you will build¶
Two services, where one needs the other. Then the same graph with a registration missing, because the interesting part of dependency injection is not how it works when it is right — it is what happens when it is wrong.
What you need¶
The setup from lesson 1. This lesson uses nothing beyond the core package.
The code¶
"""Lesson 8 — services that need other services.
Run it with:
python -m learn_fundamentals.fundamentals08_dependency_injection
"""
from __future__ import annotations
import asyncio
from typing import Annotated
from xyberos import App, Inject, MissingProviderError
class Repository:
"""A service with no dependencies of its own."""
def __init__(self) -> None:
self._names = ["ada", "linus"]
def names(self) -> list[str]:
"""Return the stored names."""
return list(self._names)
class UserService:
"""A service that declares what it needs, and nothing else."""
def __init__(self, repository: Annotated[Repository, Inject()]) -> None:
self._repository = repository
def summary(self) -> str:
"""Return the stored names as one line."""
return ", ".join(self._repository.names())
async def main() -> None:
app = App()
app.provide(Repository)
app.provide(UserService)
app.validate()
async with app.lifespan():
print("users:", (await app.resolve(UserService)).summary())
broken = App()
broken.provide(UserService)
try:
broken.validate()
except MissingProviderError as error:
print(error)
if __name__ == "__main__":
asyncio.run(main())
A service declares what it needs in its constructor, with a type annotation and an Inject() marker.
That is the whole mechanism: nothing looks anything up, and nothing depends on a parameter's name.
How to run it¶
What it prints¶
The second line is the important one. It is the same file that produced it: after the working
application stops, the module composes a second one, registers UserService without its
Repository, and lets app.validate() fail on purpose.
The lines that matter¶
| Rule | Why it is a rule |
|---|---|
Every required parameter needs Annotated[T, Inject()] |
dependencies are read from the type, so nothing depends on a parameter's name |
app.validate() checks the whole graph without building anything |
a wiring mistake is a startup error, not a first-request error |
| The error names the consumer, not just the missing piece | required by UserService tells you where to look, which is usually enough to fix it without debugging |
Common mistakes¶
- Registering the consumer and not the dependency. That is the second line of output, and
MissingProviderErroris raised byvalidate(),start(), or the first resolution — whichever comes first. - Reaching for a container. There is no lookup by name and no service locator. If a service needs something, it says so in its constructor.
- Passing the application into a service. A service that holds the
Appcan resolve anything, which is the same thing as having no dependency declaration at all.
Exercise¶
Add a Formatter service that UserService depends on, and have summary() use it.
Show the answer
"""Lesson 8, exercise — a service that depends on two others.
The answer to the lesson's exercise. The graph grows by one service and the behaviour does not
change, which is the point: adding a dependency is a wiring change, not a rewrite.
"""
from __future__ import annotations
import asyncio
from typing import Annotated
from xyberos import App, Inject
class Repository:
"""A service with no dependencies of its own."""
def __init__(self) -> None:
self._names = ["ada", "linus"]
def names(self) -> list[str]:
"""Return the stored names."""
return list(self._names)
class Formatter:
"""The service the exercise adds."""
def format(self, names: list[str]) -> str:
"""Return the names as one line."""
return ", ".join(names)
class UserService:
"""A service that declares what it needs, and nothing else."""
def __init__(
self,
repository: Annotated[Repository, Inject()],
formatter: Annotated[Formatter, Inject()],
) -> None:
self._repository = repository
self._formatter = formatter
def summary(self) -> str:
"""Return the formatted names."""
return self._formatter.format(self._repository.names())
async def main() -> None:
"""Run the exercise's version of lesson 8."""
app = App()
app.provide(Repository)
app.provide(Formatter)
app.provide(UserService)
async with app.lifespan():
print("users:", (await app.resolve(UserService)).summary())
if __name__ == "__main__":
asyncio.run(main())
Next¶
Lesson 9 answers the next question: how long should a service live? Until it is published, the roadmap shows where each lesson stands.