Skip to main content

fastapi-typed-state

Type-safe application state for FastAPI lifespans, dependencies, requests, and WebSockets.

The library stores one application-scoped object in a private app.state slot. Endpoints can retrieve that object through a typed dependency or manually from any Starlette HTTPConnection.

Installation

pip install fastapi-typed-state

Python 3.12 or newer is required.

Usage

The following example loads non-secret configuration during startup, fetches database credentials from AWS Secrets Manager, and keeps a PostgreSQL pool and S3 client open for the application lifespan.

The example uses additional application dependencies that are not required by fastapi-typed-state:

pip install asyncpg aiobotocore types-aiobotocore-s3 types-aiobotocore-secrets-manager

config.toml contains identifiers and deployment configuration, but no credentials:

aws_region = 'eu-west-1'
s3_bucket = 'reports-production'
database_secret_id = 'production/reports/database'
import tomllib
from collections.abc import AsyncGenerator
from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import dataclass
from pathlib import Path

import asyncpg
from aiobotocore.session import get_session
from fastapi import FastAPI, Request
from pydantic import BaseModel, ConfigDict, PostgresDsn
from types_aiobotocore_s3.client import S3Client

import fastapi_typed_state


class ApplicationConfig(BaseModel):
    model_config = ConfigDict(frozen=True)

    aws_region: str
    s3_bucket: str
    database_secret_id: str


class DatabaseSecret(BaseModel):
    model_config = ConfigDict(frozen=True)

    dsn: PostgresDsn


@dataclass(frozen=True, slots=True)
class ApplicationState:
    config: ApplicationConfig
    database: asyncpg.Pool
    s3: S3Client


AppStateDep = fastapi_typed_state.Extracted[ApplicationState]


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
    with Path('config.toml').open('rb') as config_file:
        config = ApplicationConfig.model_validate(tomllib.load(config_file))

    aws = get_session()
    async with aws.create_client('secretsmanager', region_name=config.aws_region) as secret_manager:
        secret_response = await secret_manager.get_secret_value(SecretId=config.database_secret_id)

    secret_payload = secret_response.get('SecretString')
    if secret_payload is None:
        msg = 'Database secret does not contain SecretString'
        raise RuntimeError(msg)
    database_secret = DatabaseSecret.model_validate_json(secret_payload)

    async with AsyncExitStack() as resources:
        database = await asyncpg.create_pool(dsn=str(database_secret.dsn))
        resources.push_async_callback(database.close)
        s3 = await resources.enter_async_context(
            aws.create_client('s3', region_name=config.aws_region)
        )
        state = ApplicationState(config=config, database=database, s3=s3)

        async with fastapi_typed_state.context(app, state):
            yield


app = FastAPI(lifespan=lifespan)


@app.get('/ready')
async def ready(state: AppStateDep) -> dict[str, str]:
    database_value = await state.database.fetchval('SELECT 1')
    await state.s3.head_bucket(Bucket=state.config.s3_bucket)
    return {'database': 'ready' if database_value == 1 else 'not ready', 's3': 'ready'}


@app.get('/configuration')
async def configuration(request: Request) -> dict[str, str]:
    state = fastapi_typed_state.extract(request, ApplicationState)
    return {'aws_region': state.config.aws_region, 's3_bucket': state.config.s3_bucket}

The Secrets Manager client closes immediately after startup retrieves and validates the secret. On shutdown, context() first removes the application state, then AsyncExitStack closes the S3 client and database pool.

Importing the module keeps the intentionally concise API names explicit at each call site. The direct assignment to AppStateDep preserves FastAPI's dependency metadata at runtime and gives endpoints a short annotation. For Pyright, both AppStateDep and fastapi_typed_state.Extracted[ApplicationState] are statically the same type as ApplicationState.

At runtime, Extracted[...] retrieves the lifespan object and verifies it with isinstance. extract(connection, ApplicationState) performs the same check and has the concrete return type ApplicationState. It accepts Starlette's HTTPConnection, so it works with both FastAPI Request and WebSocket objects. Subclasses satisfy an expected base class through normal isinstance semantics.

Lifecycle

The state is available after entering context and is removed when the context exits, including exceptional exits. The supplied object is not opened or closed by this library; compose its own context manager outside context when it manages resources.

Only one fastapi-typed-state object can be active for an application. Starting a second context on the same application raises StateAlreadyInitializedError without replacing the active object. Different FastAPI applications have independent state.

Errors

Manual retrieval raises public library exceptions:

  • StateNotInitializedError when the application lifespan is not active.
  • StateTypeMismatchError when the object does not match the expected class.
  • StateAlreadyInitializedError when a second context targets the same application.

All three inherit from TypedStateError.

Dependency retrieval converts missing state and type mismatches into HTTP 500 responses with stable details. Responses do not contain the stored object or its representation.

Both Extracted[...] and extract(..., expected_type) require a concrete runtime class. Any, unions, and parameterized generics such as list[str] are not supported because they cannot be checked with ordinary isinstance semantics.

Development

uv sync
uv run pyright
uv run ruff format --check .
uv run ruff check .
uv run pytest
uv build

Pyright runs in strict mode using its Node.js extra. Ruff checks all rules except return annotations, docstrings, copyright, security, and lazy-import rules; formatting uses spaces, LF line endings, a 100-character line length, single quotes, and no magic trailing comma.

Download files

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

Source Distribution

fastapi_typed_state-0.2.0.tar.gz (9.3 kB view details)

Uploaded Source

Built Distribution

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

fastapi_typed_state-0.2.0-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_typed_state-0.2.0.tar.gz.

File metadata

  • Download URL: fastapi_typed_state-0.2.0.tar.gz
  • Upload date:
  • Size: 9.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for fastapi_typed_state-0.2.0.tar.gz
Algorithm Hash digest
SHA256 cfeb7a65778d73b9f6070fe5ea82b4344a843b5b1a763f6d4881f4bab18faeef
MD5 5a2d93a0cb70f5ecb9e85268e2a16f49
BLAKE2b-256 108f0280842cecb8574ab0df472c211f91e4c7c69d59531a600d2efda6a99914

See more details on using hashes here.

File details

Details for the file fastapi_typed_state-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_typed_state-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 69daa292ca77a102a62308519760f320ef08f5f0c06235a0632749a0277fbfc1
MD5 8ad8f66f66070ae00e9f28d24915b316
BLAKE2b-256 8f4350ca1de1895056d69e2531336769e914f4b08c70e4bfec7cfb1911fd219f

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 Sentry Error logging StatusPage Status page