Skip to main content

Application-scoped dependencies for FastAPI

Project description

fastapi-singleton

Application-scoped dependencies for fastapi

Every dependency resolved through FastAPI's Depends is request-scoped: created on each request and discarded once the response is sent. That's the right default for most things, but it's the wrong default for connection pools, HTTP clients, and anything else that's expensive to create and safe to share.

fastapi-singleton gives you a @singleton decorator that turns any dependency, function or class, into one shared instance per process, with proper startup and shutdown hooks wired into FastAPI's lifespan, instead of leaving it to whatever a SIGTERM does to a @lru_cached object.

Example

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi_singleton import singleton, lifespan


@singleton
class Settings:
    def __init__(self):
        self.dsn = "postgresql://localhost/app"


@singleton
async def get_pool(settings: Annotated[Settings, Depends(Settings)]):
    pool = await create_pool(settings.dsn)
    yield pool
    await pool.close()


@get_pool.before_start
def log_pool_starting():
    logger.info("opening connection pool")


@get_pool.after_end
def log_pool_closed():
    logger.info("connection pool closed")


app = FastAPI(lifespan=lifespan)


@app.get("/users/{user_id}")
def read_user(pool: Annotated[Pool, Depends(get_pool)], user_id: int):
    return pool.fetch_user(user_id)
$ uvicorn app:app

INFO:     opening connection pool
INFO:     Application startup complete.
...
INFO:     Shutting down
INFO:     connection pool closed
INFO:     Application shutdown complete.

Installation

uv add fastapi-singleton

Defining a singleton

@singleton wraps a function or a class so that it's only ever called once per process; every dependant that resolves it via Depends receives the exact same instance, the same guarantee @lru_cache(maxsize=1) gives you, but tracked in a registry so its lifecycle can be managed instead of left to the garbage collector.

@singleton
def get_other():
    return Other()

Singletons can depend on other singletons the same way any FastAPI dependency does, by declaring them with Depends in the constructor or function signature:

@singleton
class Connection:
    def __init__(self, other: Annotated[Other, Depends(get_other)]):
        self.other = other

A class singleton's __init__ is the constructor, plain and simple - Depends(Connection) calls it exactly once, the same way any FastAPI class-based dependency works. __init__ can never be async def in Python, so a class singleton can't do real async setup itself - if you need that (an async connection pool, an await-based client, anything with teardown), write it as a function singleton instead and have your class depend on it, the same way Connection depends on get_other above.

A singleton can't depend on a regular, request-scoped dependency - there's no request to resolve it from when the singleton is constructed eagerly at startup, or directly in plain Python. @singleton-ing something that depends on non-singleton Depends(...) raises an error rather than silently resolving it once and reusing stale data on every later request.

A singleton is also constructed exactly once: calling it again with the same arguments it was first constructed with is a no-op (this is what lets FastAPI re-resolve a singleton's own Depends-declared dependencies on every request without recreating anything), but calling it again with genuinely different arguments raises rather than silently ignoring them.

Teardown with generators

If a singleton needs to release what it acquired, write it as a generator, exactly like a request-scoped yield dependency in FastAPI. The code before yield runs once, on creation; the code after yield runs once, on shutdown.

@singleton
def get_other():
    other = Other()
    yield other
    other.close()

Lifecycle hooks

Sometimes the setup or teardown you need isn't part of constructing the resource itself, things like metrics, logging, or cache warming. Each singleton exposes hooks you can register without touching its body:

@Connection.before_start
def before_start():
    ...  # runs immediately before Connection is constructed


@get_other.before_end
def before_end():
    ...  # runs immediately before get_other's teardown executes


@get_other.after_end
def after_end():
    ...  # runs immediately after get_other's teardown completes
Hook Runs
before_start Immediately before the singleton is constructed
before_end Immediately before the singleton's teardown executes
after_end Immediately after the singleton's teardown completes

A singleton can register any number of hooks for each event; they run in registration order.

Wiring up the lifespan

Singletons are created lazily by default, on first resolution, the same as @lru_cache. To get deterministic startup and shutdown instead, pass fastapi_singleton.lifespan to your FastAPI app:

from fastapi_singleton import lifespan

app = FastAPI(lifespan=lifespan)

On startup, every registered singleton is constructed eagerly, in dependency order, so a connection pool is open and ready before the app accepts its first request. On shutdown, each singleton is torn down in reverse order, running any before_end hooks, its own post-yield teardown, then any after_end hooks, like a stack of context managers being unwound.

If you already have a lifespan of your own, compose them:

from contextlib import asynccontextmanager

from fastapi_singleton import lifespan as singleton_lifespan


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with singleton_lifespan(app):
        # your own startup
        yield
        # your own shutdown


app = FastAPI(lifespan=lifespan)

Without the lifespan wired up, singletons still work, lazily, on first call, like a plain @lru_cache, but nothing guarantees their teardown code runs; register the lifespan whenever a singleton's cleanup actually matters.

One process, one app

Singletons live in a process-global registry, the same way @lru_cached state does. That makes fastapi-singleton a fit for one FastAPI app per process; running two FastAPI(lifespan=lifespan) apps side by side in the same process means they'd share singleton state, including teardown. If you're testing code that uses singletons, reset the registry between tests:

from fastapi_singleton import reset


@pytest.fixture(autouse=True)
def reset_singletons():
    reset()

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_singleton-0.1.0.tar.gz (10.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_singleton-0.1.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_singleton-0.1.0.tar.gz.

File metadata

  • Download URL: fastapi_singleton-0.1.0.tar.gz
  • Upload date:
  • Size: 10.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fastapi_singleton-0.1.0.tar.gz
Algorithm Hash digest
SHA256 970a08eeffd33a593caccc39f4eedbb2bcfc10484504d5970e60e70a5f72ab61
MD5 4261d3f22170401cefc4636526392a79
BLAKE2b-256 bbcc4e184ba595c697b31e819a66dfe1e2e306a24a8f7c5e3f6cdd9c4efb584e

See more details on using hashes here.

File details

Details for the file fastapi_singleton-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: fastapi_singleton-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fastapi_singleton-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eeb3fa5f979f2b9905f0cecd4b79f7876c6fe39238072de65c81f3363d7f5e8f
MD5 ae6b66feb3de72acbc33e9f6006dd114
BLAKE2b-256 dc8219fff2c026e557b5170cedb386b2860c94661f455aa88eef367dcf2886e1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page