Why?#

Monkey patching is software bankruptcy.

—Brandon Rhodes

svcs (pronounced services) gives you unified and ergonomic API for storing and retrieving objects to and from your web application’s request objects. Additionally, it ensures that those objects get cleaned up when the request is done, offers hooks that make your application more testable, and gives you live introspection of their health.

This documentation mostly talks in terms of web applications because they are the most common use-case for packages like this. However, svcs is useful for any application that can benefit from late binding and being pluggable. The word flexible is part of the project’s tagline for a reason! It’s bound neither to request objects nor to web applications.


More formally: svcs is a service locator. Service locators like svcs allow you to configure and manage all your services in one central place, acquire them in a consistent way without worrying about cleaning them up, and thus achieve loose coupling. That gives you a well-defined place and method for storing – and replacing! – your application’s configurable dependencies.

Terminology

If the term service seems confusing to you, it’s because it is. The term is so overloaded in software engineering that it can mean everything and nothing. But it’s the correct term, so we’re using it to avoid making things even more confusing.

If you want the full scoop, we have an extensive glossary that explains what we mean by service in the context of svcs. But for now, you can think of it as a configurable dependency that your application needs to do things like accessing databases or web APIs, and you’ll be able to follow along just fine.

Service location is not related to service discovery.

Modus Operandi#

In practice, you say “svcs, give me a database connection!” once you need a database connection, and svcs will give you whatever you’ve configured to return when asked for a database connection. That can be an actual database connection, or it can be a fake test object.

A key feature of service locators is that you only ask for the services once you know that you will need them. So you don’t have to pre-instantiate all services just in case (wasteful!) or move the instantiation further into, for example, your web views (onerous resource management!).

If you follow the Dependency Inversion Principle, you would register concrete factories for abstract interfaces. In Python, that would be usually a Protocol or an abstract base class.

If you follow the Hexagonal Architecture (aka “ports and adapters”), the registered types are ports, and the factories produce the adapters.

Here’s how this looks in practice with our various integrations:

import svcs

async def view(request):
    db, api, cache = await svcs.aiohttp.aget(request, Database, WebAPIClient, Cache)

    ...
import svcs

@app.get("/")
async def view(services: svcs.fastapi.DepContainer):
    db, api, cache = await services.aget(Database, WebAPIClient, Cache)

    ...
import svcs

@app.route("/")
def view():
    db, api, cache = svcs.flask.get(Database, WebAPIClient, Cache)

    ...
import svcs

@view_config(route_name="index")
def view(request):
    db, api, cache = svcs.pyramid.get(request, Database, WebAPIClient, Cache)

    ...
import svcs

async def view(request):
    db, api, cache = await svcs.starlette.aget(request, Database, WebAPIClient, Cache)

    ...

To a type checker like Mypy, db has the type Database, api has the type WebAPIClient, and cache has the type Cache.

You set it up like this:

import atexit

from sqlalchemy import Connection, create_engine, text

...

engine = create_engine("postgresql://localhost")

def connection_factory():
    with engine.connect() as conn:
        yield conn

registry = svcs.Registry()
registry.register_factory(
    Connection,
    connection_factory,
    ping=lambda conn: conn.execute(text("SELECT 1")), # ← health check
    on_registry_close=engine.dispose
)

@atexit.register
def cleanup():
    registry.close()  # calls engine.dispose()

The generator-based setup and cleanup may remind you of pytest fixtures. However, internally svcs uses context managers to manage cleanups, and if you pass a generator, it just wraps it with contextmanager() for your convenience. But of course, you can directly pass a context manager as a factory, too, and the following is equivalent to the above:

engine = create_engine("postgresql://localhost")

registry = svcs.Registry()
registry.register_factory(
    Connection,
    engine.connect,  # ← sqlalchemy.Connection is a context manager
    ping=lambda conn: conn.execute(text("SELECT 1")),
    on_registry_close=engine.dispose
)

@atexit.register
def cleanup():
    registry.close()

The automatic entering of context managers can be disabled on registration if you need control over when they’re entered (for example, for database transaction managers).

The callbacks defined as on_registry_close are called when you call svcs.Registry.close() – for example, when your application is shutting down or after a test.

Next, you can write a simple health check endpoint if you’ve registered health checks (called pings) for your services. This is how it could look with the shipped integrations:

from __future__ import annotations

from aiohttp.web import Request, Response, json_response

import svcs


async def healthy_view(request: Request) -> Response:
    """
    Ping all external services.
    """
    ok: list[str] = []
    failing: list[dict[str, str]] = []
    code = 200

    for svc in svcs.aiohttp.get_pings(request):
        try:
            await svc.aping()
            ok.append(svc.name)
        except Exception as e:
            failing.append({svc.name: repr(e)})
            code = 500

    return json_response({"ok": ok, "failing": failing}, status=code)
from __future__ import annotations

from fastapi import FastAPI
from fastapi.responses import JSONResponse

import svcs


app = FastAPI(...)


@app.get("/healthy")
async def healthy(services: svcs.fastapi.DepContainer) -> JSONResponse:
    """
    Ping all external services.
    """
    ok: list[str] = []
    failing: list[dict[str, str]] = []
    code = 200

    for svc in services.get_pings():
        try:
            await svc.aping()
            ok.append(svc.name)
        except Exception as e:
            failing.append({svc.name: repr(e)})
            code = 500

    return JSONResponse(
        content={"ok": ok, "failing": failing}, status_code=code
    )
from __future__ import annotations

import flask

import svcs


bp = flask.Blueprint("instrumentation", __name__)


@bp.get("healthy")
def healthy() -> flask.ResponseValue:
    """
    Ping all external services.
    """
    ok: list[str] = []
    failing: list[dict[str, str]] = []
    code = 200

    for svc in svcs.flask.get_pings():
        try:
            svc.ping()
            ok.append(svc.name)
        except Exception as e:
            failing.append({svc.name: repr(e)})
            code = 500

    return {"ok": ok, "failing": failing}, code
from __future__ import annotations

import json

from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config

import svcs


@view_config(route_name="healthy")
def healthy_view(request: Request) -> Response:
    """
    Ping all external services.
    """
    ok: list[str] = []
    failing: list[dict[str, str]] = []
    status = 200

    for svc in svcs.pyramid.get_pings(request):
        try:
            svc.ping()
            ok.append(svc.name)
        except Exception as e:
            failing.append({svc.name: repr(e)})
            status = 500

    return Response(
        content_type="application/json",
        status=status,
        body=json.dumps({"ok": ok, "failing": failing}).encode("ascii"),
    )
from __future__ import annotations

from starlette.requests import Request
from starlette.responses import JSONResponse

import svcs


async def healthy(request: Request) -> JSONResponse:
    """
    Ping all external services.
    """
    ok: list[str] = []
    failing: list[dict[str, str]] = []
    code = 200

    for svc in svcs.starlette.get_pings(request):
        try:
            await svc.aping()
            ok.append(svc.name)
        except Exception as e:
            failing.append({svc.name: repr(e)})
            code = 500

    return JSONResponse(
        content={"ok": ok, "failing": failing}, status_code=code
    )

Once written, you never touch this view endpoint again and define the service health checks where you define the services.

Important

All of this may look over-engineered if you have only one or two services. However, it starts paying dividends very fast once you go past that.

asyncio#

svcs comes with full async support via a-prefixed methods (like aget() instead of get(), and so on).

In fact, most of our Integrations are for async frameworks!

Static Typing#

While svcs also has first-class support for static typing, it is strictly optional and will always remain so. svcs also doesn’t check your types at runtime. It only forwards the type you have asked for to the type checker. If you don’t use a type checker, that information is ignored without any runtime overhead.

Is this Dependency Injection or Service Location!?#

It can be both, depending on your perspective! At its core, svcs is a service locator because it locates services for you when you call get() – based on your configuration.

But it depends where you choose to call get() whether you’re doing dependency injection or service location in the classical sense.

When people think of dependency injection, they usually think of dependency injection frameworks that use decorators, parameter inspection, and other magic to inject services automatically into their code. But that’s not what dependency injection means. It means that a piece of code doesn’t instantiate its dependencies itself and is called with all services it needs to do its job (also known as Inversion of Control).

Therefore, if you use svcs in your web view to look up a database connection and pass the database connection into your service layer, you’re doing dependency injection for the arguably most important part of your application. You have moved your composition root into the web view, which allows you to be more flexible with the acquisition of your services while maintaining loose coupling between your service layer and its dependencies.

On the other hand, if you use svcs in your service layer – or even business logic – to look up a database connection and use it there, you’re doing service location.

We strongly recommend the former over the latter because it’s much easier to test and reason about.

If you’re curious, check the glossary entries for Service Locator and Dependency Injection for more details.

Benefits#

While it may take a moment to realize, all of this comes with many benefits:

Reduction in boilerplate. Every web framework has some way to store data on long-lived objects like the application, and short-lived objects like requests (for example, in Flask it’s flask.Flask.extensions and flask.g – Starlette uses request.state for both). Some frameworks also have helpers to control the lifecycle of those objects (like flask.Flask.teardown_appcontext() or pyramid.request.Request.add_finished_callback()). But they work subtly differently and you accumulate a lot of repetitive boilerplate code. In fact, Hynek started this project because of the repetitiveness of Flask’s get_X pattern.

Unification of acquisition and release. Knowing where to find your services, how to acquire them, and not caring about their cleanup makes your application more robust and easier to reason about. It also makes it easier to write reusable middleware because you don’t have to remember where a dependency it needs is stored on the request object (or was it on the application object!?). With svcs you just have to remember its type and gain a portable API for pluggable dependencies.

Type safety. Since you’re asking for objects of certain types, svcs can ensure that Mypy knows that the returned object is of that type. You can cheat, of course, by returning something else – svcs doesn’t care. And, of course, type hints are optional – svcs is just as valuable without them.

Unintrusive testing through loose coupling. As per Brandon’s quote at the beginning of this section, monkey-patching is software bankruptcy. Adding late binding to your application allows you to replace your dependencies with test objects in a well-defined, debuggable way. Just create your application and overwrite the service configurations before you perform your tests as necessary.

Health checks. A production-ready application should be able to tell you whether it – and all its external dependencies – is healthy. Having that exposed as a web endpoint is great for monitoring and debugging. Providing a health endpoint without a centralized registry of services is highly boilerplate-heavy. With svcs you get that for free.

Why not?#

The main downside of service locators is that it’s only possible to verify whether all required dependencies have been configured by running the code.

That’s a consequence of late binding happening imperatively and the main trade-off when deciding between a service locator like svcs and a traditional dependency injection framework that is usually declarative and knows the requirements ahead of time.

We believe the upsides of service locators outweigh the downsides and that avoiding late binding problems is easy. For instance, by configuring the same service in the same place for all environments.

If you still prefer a dependency injection framework, check out incant – a lovely package by a friend of the project.

What Next?#

If you’re still interested, learn about our core concepts first – it’s just two of them!

Once you’ve understood the life cycles of registries and containers, you can look our framework integrations which should get you started right away.

Whenever you get overwhelmed by the jargon, we have put much effort into our glossary!