Getting Started with Decorators

The examples below build a single web application step by step, introducing one concept at a time. All code is runnable with the standard library only.

Step 1 — Define services

Decorate a class with @scope to make it a DI container. Annotate each value with @resource and expose it with @public. Resources declare their dependencies as ordinary function parameters; the framework injects them by name.

Use @extern to declare a dependency that must come from outside the scope — the equivalent of a pytest fixture parameter. Pass multiple scopes to evaluate() to compose them; dependencies are resolved by name across scope boundaries. Config values are passed as kwargs when calling the evaluated scope.

@scope
class SQLiteDatabase:
    @extern
    def databasePath() -> str: ...       # caller must provide this

    @public
    @resource
    def connection(databasePath: str) -> sqlite3.Connection:
        return sqlite3.connect(databasePath)

@scope
class UserRepository:
    @public
    @resource
    def userCount(connection: sqlite3.Connection) -> int:
        connection.execute(
            "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"
        )
        (count,) = connection.execute("SELECT COUNT(*) FROM users").fetchone()
        return count

SQLiteDatabase owns databasePath; UserRepository has no knowledge of the database layer — it only declares connection: sqlite3.Connection as a parameter and receives it automatically from the composed scope.

Tip

Naming convention

Throughout this tutorial we use UpperCamelCase for scopes and lowerCamelCase for resources. The idea is that a scope is conceptually a class — an instantiable container — while a resource is a lazily-evaluated value inside it.

In the example above, SQLiteDatabase and UserRepository are scopes (UpperCamelCase), while databasePath, connection, and userCount are resources (lowerCamelCase).

This convention extends to Python modules used as scopes: a module file representing a scope is named in UpperCamelCase (e.g., SqliteDatabase.py), and a subpackage representing a nested scope likewise (e.g., UserRepository/Request/). This deviates from PEP 8 — the MIXINv2 decorators form a DSL, and the casing signals that the code is not plain Python data model.

Step 2 — Layer cross-cutting concerns with @patch and @merge

@patch wraps an existing resource value with a transformation. This lets an add-on scope modify a value without touching the scope that defined it — the same idea as pytest’s monkeypatch, but composable.

@scope
class Base:
    @public
    @resource
    def maxConnections() -> int:
        return 10

@scope
class HighLoad:
    """Patch for high-load environments: double the connection limit."""

    @patch
    def maxConnections() -> Callable[[int], int]:
        return lambda previous: previous * 2

When several independent scopes each contribute a piece to the same resource, use @merge to define how the contributions are aggregated:

@scope
class PragmaBase:
    @public
    @merge
    def startupPragmas() -> Callable[[Iterator[str]], frozenset[str]]:
        return frozenset                  # aggregation strategy: collect into frozenset

@scope
class WalMode:
    @patch
    def startupPragmas() -> str:
        return "PRAGMA journal_mode=WAL"

@scope
class ForeignKeys:
    @patch
    def startupPragmas() -> str:
        return "PRAGMA foreign_keys=ON"

A @patch can itself declare @extern dependencies, which are injected like any other resource:

@scope
class PragmaBase:
    @public
    @merge
    def startupPragmas() -> Callable[[Iterator[str]], frozenset[str]]:
        return frozenset

@scope
class UserVersionPragma:
    @extern
    def schemaVersion() -> int: ...     # provided as a kwarg at call time

    @patch
    def startupPragmas(schemaVersion: int) -> str:
        return f"PRAGMA user_version={schemaVersion}"

Step 3 — Force evaluation at startup with @eager

All resources are lazy by default: computed on first access, then cached for the lifetime of the scope. Mark a resource @eager to evaluate it immediately when evaluate() returns — useful for schema migrations or connection pre-warming that must complete before the application starts serving requests:

@scope
class SQLiteDatabase:
    @public
    @eager
    @resource
    def connection() -> sqlite3.Connection:
        db = sqlite3.connect(":memory:")
        db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
        db.commit()
        return db

Without @eager, the CREATE TABLE would not run until root.connection is first accessed.

Step 4 — App scope vs request scope

So far all resources have had application lifetime: created once at startup and reused for every request. Real applications also need per-request resources — values that must be created fresh for each incoming request and discarded when it completes.

A nested @scope named Request serves as a per-request factory. The framework injects it by name as a Callable; calling Request(request=handler) returns a fresh instance.

The application below has four scopes, each owning only its own concern:

  • SQLiteDatabase — owns databasePath, provides connection

  • UserRepository — business logic; owns userCount and per-request currentUser

  • HttpHandlers — HTTP layer; owns per-request userId, responseBody, responseSent

  • NetworkServer — network layer; owns host/port, creates the HTTPServer

UserRepository.Request and HttpHandlers.Request are composed into a single Request by the union mount. userId (extracted from the HTTP path by HttpHandlers.Request) flows automatically into currentUser (looked up in the DB by UserRepository.Request) without any glue code.

responseSent is an IO resource: it sends the HTTP response as a side effect and returns None. The handler body is a single attribute access — all logic lives in the DI graph. In an async framework (e.g. FastAPI), return an asyncio.Task[None] instead of a coroutine, which cannot be safely awaited in multiple dependents.

@scope
class SQLiteDatabase:
    @extern
    def databasePath() -> str: ...    # database owns its own config

    # App-scoped: one connection for the entire process lifetime.
    # check_same_thread=False: created in main thread, used in handler threads.
    @public
    @resource
    def connection(databasePath: str) -> sqlite3.Connection:
        db = sqlite3.connect(databasePath, check_same_thread=False)
        db.execute(
            "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
        )
        db.execute("INSERT INTO users VALUES (1, 'alice')")
        db.execute("INSERT INTO users VALUES (2, 'bob')")
        db.commit()
        return db

@scope
class UserRepository:
    @extern
    def connection() -> sqlite3.Connection: ...

    # @scope as a composable dataclass — fields are @extern, constructed via DI.
    @public
    @scope
    class User:
        @public
        @extern
        def userId() -> int: ...

        @public
        @extern
        def name() -> str: ...

    # App-scoped: total count across all requests.
    @public
    @resource
    def userCount(connection: sqlite3.Connection) -> int:
        (count,) = connection.execute(
            "SELECT COUNT(*) FROM users"
        ).fetchone()
        return count

    # Request-scoped: per-request DB resources, wired by overlay union-mount.
    @public
    @scope
    class Request:
        @extern
        def userId() -> int: ...  # provided by HttpHandlers.Request

        @public
        @resource
        def currentUser(
            connection: sqlite3.Connection, userId: int, User: Callable
        ) -> object:
            row = connection.execute(
                "SELECT id, name FROM users WHERE id = ?", (userId,)
            ).fetchone()
            assert row is not None, f"no user with id={userId}"
            identifier, name = row
            return User(userId=identifier, name=name)

@scope
class HttpHandlers:
    @extern
    def userCount() -> int: ...

    # Request is nested because its lifetime is per-request,
    # not per-application.
    @public
    @scope
    class Request:
        class _RequestWithPath(Protocol):
            path: str

        @extern
        def request() -> BaseHTTPRequestHandler: ...

        @extern
        def currentUser() -> object: ...

        # userId is extracted from the request and injected into
        # UserRepository.Request.currentUser automatically.
        @public
        @resource
        def userId(request: _RequestWithPath) -> int:
            return int(request.path.split("/")[-1])

        @public
        @resource
        def responseBody(userCount: int, currentUser: object) -> bytes:
            return (
                f"total={userCount} current={currentUser.name}"
            ).encode()

        # IO resource: sends the HTTP response as a side effect.
        @public
        @resource
        def responseSent(
            request: BaseHTTPRequestHandler,
            responseBody: bytes,
        ) -> None:
            request.send_response(200)
            request.end_headers()
            request.wfile.write(responseBody)

@scope
class NetworkServer:
    @extern
    def host() -> str: ...             # network layer owns its own config

    @extern
    def port() -> int: ...

    @scope
    class Request:
        pass

    # Request is injected by name as a Callable (StaticScope).
    # Calling Request(request=handler) returns a fresh InstanceScope.
    @public
    @resource
    def server(host: str, port: int, Request: Callable) -> HTTPServer:
        class Handler(BaseHTTPRequestHandler):
            def do_GET(self) -> None:
                Request(request=self).responseSent

            def log_message(self, format: str, *arguments: object) -> None:
                pass

        return HTTPServer((host, port), Handler)

@extend(
    LexicalReference(path=("SQLiteDatabase",)),
    LexicalReference(path=("UserRepository",)),
    LexicalReference(path=("HttpHandlers",)),
    LexicalReference(path=("NetworkServer",)),
)
@public
@scope
class App:
    pass

Assemble into a module and evaluate — pass the module directly to evaluate():

import mixinv2_examples.app_decorator.step4_http_server as step4_http_server

root = evaluate(step4_http_server, modules_public=True).App(
    databasePath="/var/lib/myapp/prod.db",
    host="127.0.0.1",
    port=8080,
)
server = root.server

Swapping to a test configuration is just different kwargs; no scope or composition changes:

test_root = evaluate(step4_http_server, modules_public=True).App(
    databasePath=":memory:",  # fresh, isolated database for each test
    host="127.0.0.1",
    port=0,                    # OS assigns a free port
)
# test_root.connection  → sqlite3.Connection to :memory:
# test_root.server      → HTTPServer on OS-assigned port

Decorator reference

Decorator

Purpose

@scope

Define a DI container (class) or sub-namespace

@resource

Declare a lazily-computed value; parameters are injected by name

@public

Expose a @resource or @scope to external callers

@extern

Declare a required dependency that must come from the composed scope

@patch

Provide a transformation that wraps an existing resource

@patch_many

Like @patch but yields multiple transformations at once

@merge

Define how patches are aggregated (e.g. frozenset, list, custom reducer)

@eager

Force evaluation at scope creation rather than on first access

@extend(*refs)

Inherit from other scopes explicitly (for package-level union mounts)

evaluate(*scopes)

Resolve and union-mount one or more scopes into a single dependency graph

Python modules as scopes

The @scope classes above are a teaching convenience — the real-world style is plain Python modules, just like pytest fixtures don’t require a class. Every @scope class maps directly to a module file; pass it to evaluate() the same way:

import SqliteDatabase   # SqliteDatabase.py with @extern / @resource / @public
import UserRepository   # UserRepository/ package

The same decorators work on module-level functions exactly as on class methods. A subpackage becomes a nested scope — UserRepository/Request/ is the module equivalent of a nested @scope class Request.

Use @extend in a package’s __init__.py to declare the composition, then evaluate() receives the single package:

@extend(
    LexicalReference(path=("SqliteDatabase",)),
    LexicalReference(path=("UserRepository",)),
)
@public
@scope
class Step1App:
    pass
import myapp

root = evaluate(myapp, modules_public=True).App(databasePath=":memory:")

Runnable module-based equivalents of all tutorial examples are in packages/mixinv2-examples/tests/test_readme_package_examples.py, using the fixture package at packages/mixinv2-examples/src/mixinv2_examples/app_di/.