Skip to content

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

examples/learn_fundamentals/fundamentals08_dependency_injection.py
"""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

$env:PYTHONPATH = "src;examples"
python -m learn_fundamentals.fundamentals08_dependency_injection
PYTHONPATH=src:examples python -m learn_fundamentals.fundamentals08_dependency_injection

What it prints

users: ada, linus
no provider is registered for Repository; required by UserService

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 MissingProviderError is raised by validate(), 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 App can 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
examples/learn_fundamentals/fundamentals08_exercise.py
"""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())
The output does not change — and that is the point of the exercise: the graph grew by one service and the behaviour stayed the same.
users: ada, linus

Next

Lesson 9 answers the next question: how long should a service live? Until it is published, the roadmap shows where each lesson stands.