Skip to main content

pydantic-di

A lightweight dependency loading and injection package for Python.

pydantic-di provides a small dependency system for loading objects from environment variables, Python callables, Pydantic models, attrs classes, and custom loaders. It is designed to feel familiar if you have used FastAPI dependencies, while also working outside FastAPI.

Migration from auth-broker

As of pydantic-di version 0.2.2, this package has moved out of the auth-broker organisation, been renamed, and had its import namespace updated.

Item Previous Current
GitHub repository auth-broker/package-dependency mattcoulter7/pydantic-di
PyPI package ab-dependency pydantic-di
Install command pip install ab-dependency pip install pydantic-di
Import namespace ab_core.dependency pydantic_di

The old PyPI package is retained as an archived historical package. New work should use pydantic-di and pydantic_di.

Features

  • Load Pydantic models from environment variables
  • Load Pydantic models from local JSON, YAML, TOML, and INI files
  • Load primitive values from environment variables
  • Support discriminated unions
  • Support attrs classes by converting them to Pydantic-compatible models
  • Support singleton-style persistent dependencies
  • Support transient dependencies
  • Inject dependencies into:
    • sync functions
    • async functions
    • sync generators
    • async generators
    • classes
    • Pydantic models
  • Support generator dependency cleanup
  • Support FastAPI dependency integration
  • Support flattened environment variable conventions
  • Support JSON serialised complex values, such as lists

Installation

pip install pydantic-di

Or with uv:

uv add pydantic-di

Basic usage

from pydantic import BaseModel

from pydantic_di import Load


class AppConfig(BaseModel):
    host: str = "localhost"
    port: int = 8080


config = Load(AppConfig)

print(config.host)
print(config.port)

By default, object models are loaded from environment variables using the model name converted to env-var style.

For AppConfig, the default prefix is:

APP_CONFIG

So these environment variables:

APP_CONFIG_HOST=0.0.0.0
APP_CONFIG_PORT=8000

produce:

AppConfig(host="0.0.0.0", port=8000)

Environment variable naming

Model names are converted from PascalCase or camelCase to uppercase snake case.

OAuth2TokenStore -> O_AUTH2_TOKEN_STORE
HTTPServerConfig -> HTTP_SERVER_CONFIG
AppConfig        -> APP_CONFIG

Field names are appended to the prefix.

APP_CONFIG_HOST=0.0.0.0
APP_CONFIG_PORT=8000

Nested field names are flattened using underscores.

class DatabaseConfig(BaseModel):
    host: str
    port: int


class AppConfig(BaseModel):
    database: DatabaseConfig
APP_CONFIG_DATABASE_HOST=localhost
APP_CONFIG_DATABASE_PORT=5432

Loading primitive values

Use LoaderEnvironment when loading a single primitive value from a specific environment variable.

from pydantic_di.loaders import LoaderEnvironment

port = LoaderEnvironment[int](key="PORT").load()
PORT=8080

The value is validated and cast using Pydantic.

Loading objects from local files

Use the file-backed object loaders when structured configuration already lives in a local file.

from pydantic import BaseModel

from pydantic_di.loaders import ObjectLoaderJson


class Credentials(BaseModel):
    username: str
    password: str


class ServiceConfig(BaseModel):
    host: str
    port: int
    credentials: Credentials


config = ObjectLoaderJson[ServiceConfig](
    path="config.json",
).load()

Each file loader parses its source into a dictionary, then uses the same Pydantic validation pipeline as the environment object loader.

Supported object file loaders:

Loader Source value Format
ObjectLoaderJson JSON_OBJECT JSON
ObjectLoaderYaml YAML_OBJECT YAML, parsed with yaml.safe_load
ObjectLoaderToml TOML_OBJECT TOML, parsed with stdlib tomllib
ObjectLoaderIni INI_OBJECT INI, parsed with ConfigParser

The path field is a Path, but strings are accepted and converted by Pydantic.

from pathlib import Path

ObjectLoaderJson[ServiceConfig](path="config.json")
ObjectLoaderJson[ServiceConfig](path=Path("config.json"))

JSON files

{
  "host": "localhost",
  "port": 5432,
  "credentials": {
    "username": "admin",
    "password": "secret"
  }
}
from pydantic_di.loaders import ObjectLoaderJson

config = ObjectLoaderJson[ServiceConfig](
    path="config.json",
).load()

YAML files

host: localhost
port: 5432
credentials:
  username: admin
  password: secret
from pydantic_di.loaders import ObjectLoaderYaml

config = ObjectLoaderYaml[ServiceConfig](
    path="config.yaml",
).load()

YAML is parsed with yaml.safe_load.

TOML files

host = "localhost"
port = 5432

[credentials]
username = "admin"
password = "secret"
from pydantic_di.loaders import ObjectLoaderToml

config = ObjectLoaderToml[ServiceConfig](
    path="config.toml",
).load()

INI files

[DEFAULT]
host = localhost
port = 5432

[credentials]
username = admin
password = secret
from pydantic_di.loaders import ObjectLoaderIni

config = ObjectLoaderIni[ServiceConfig](
    path="config.ini",
).load()

INI values are read as strings first, then Pydantic casts them to the target field types.

File shape

File-backed object loaders expect nested objects to be represented as nested file data.

credentials:
  username: admin
  password: secret

Environment-style flattened nested keys are not expanded for file loaders.

credentials_username: admin
credentials_password: secret

Field names that contain underscores still work normally.

class AppConfig(BaseModel):
    api_key: str
api_key: secret

The field-alignment helper can also accept a nested representation for underscore field names.

api:
  key: secret

Both forms can resolve to api_key, but nested models should use real nested objects.

Loader defaults

There are two default mechanisms with different semantics:

  • default_value: whole-result fallback when no source value is loaded.
  • default_values: object-field defaults for model loaders, merged with loaded values.

Whole-result fallback (default_value)

Use this for scalar or single-value loaders.

from pydantic_di.loaders import LoaderEnvironment

port = LoaderEnvironment[int](
    key="PORT",
    default_value=8080,
).load()

If PORT is missing, this returns 8080.

Falsy defaults are supported, including 0, False, and empty strings.

Partial object defaults (default_values)

Use this with object loaders to provide field-level defaults that are merged with loaded values.

from pydantic import BaseModel
from pydantic_di.loaders import ObjectLoaderEnvironment


class Credentials(BaseModel):
    username: str
    password: str


class ServiceConfig(BaseModel):
    api_key: str
    port: int
    credentials: Credentials


config = ObjectLoaderEnvironment[ServiceConfig](
    env_prefix="MY_CONFIG",
    default_values={
        "api_key": "default",
        "port": 5432,
        "credentials": {
            "username": "admin",
            "password": "secret",
        },
    },
).load()

With:

MY_CONFIG_PORT=6432
MY_CONFIG_CREDENTIALS_USERNAME=matt

The effective data is:

{
    "api_key": "default",  # from default_values
    "port": "6432",  # from env
    "credentials": {
        "username": "matt",  # from env
        "password": "secret",  # from default_values
    },
}

Important notes:

  • default_values should be provided in model-field shape.
  • Environment values always override defaults for matching leaves.
  • Environment key flattening and field alignment are applied to loaded source data.

Persistent dependencies

Load(..., persist=True) caches the loaded dependency.

from pydantic import BaseModel
from pydantic_di import Load


class Client(BaseModel):
    name: str = "client"


one = Load(Client, persist=True)
two = Load(Client, persist=True)

assert one is two

Transient dependencies are created each time.

one = Load(Client, persist=False)
two = Load(Client, persist=False)

assert one is not two
assert one == two

Lazy dependencies

Use Depends to defer loading until call time.

from typing import Annotated

from pydantic import BaseModel

from pydantic_di import Depends, inject


class Settings(BaseModel):
    value: str = "hello"


@inject
def run(settings: Annotated[Settings, Depends(Settings)]):
    return settings.value


assert run() == "hello"

Function injection

from typing import Annotated

from pydantic import BaseModel

from pydantic_di import Depends, inject


class Database(BaseModel):
    url: str = "sqlite://"


@inject
def handler(db: Annotated[Database, Depends(Database)]):
    return db.url

Dependencies are only resolved when the argument was not explicitly provided.

handler(Database(url="postgresql://"))

Async function injection

from typing import Annotated

from pydantic_di import Depends, inject


async def make_token() -> str:
    return "abc"


@inject
async def handler(token: Annotated[str, Depends(make_token)]):
    return token

Generator dependency support

Generator dependencies are entered before the function runs and cleaned up afterwards.

from typing import Annotated

from pydantic_di import Depends, inject


def resource():
    try:
        yield "resource"
    finally:
        print("closed")


@inject
def handler(value: Annotated[str, Depends(resource)]):
    return value

Exceptions are thrown back into the generator so except and finally blocks can run.

def resource():
    try:
        yield "resource"
    except Exception:
        print("caught")
        raise
    finally:
        print("closed")

Class injection

from typing import Annotated

from pydantic import BaseModel

from pydantic_di import Depends, inject


class Settings(BaseModel):
    value: str = "hello"


@inject
class Service:
    settings: Annotated[Settings, Depends(Settings)]

    def run(self):
        return self.settings.value

Pydantic model injection

from typing import Annotated

from pydantic import BaseModel

from pydantic_di import Depends, inject


class Settings(BaseModel):
    value: str = "hello"


@inject
class AppConfig(BaseModel):
    settings: Annotated[Settings, Depends(Settings)]
    retries: int = 3

If a field is supplied by input data, the dependency is not resolved.

FastAPI integration

Depends subclasses FastAPI's dependency parameter when FastAPI is installed.

from typing import Annotated

from fastapi import Depends as FDepends, FastAPI
from pydantic import BaseModel

from pydantic_di import Depends, inject


class SomeDependency(BaseModel):
    value: str = "injected"


def provide_dependency() -> SomeDependency:
    return SomeDependency()


@inject
def context(dep: Annotated[SomeDependency, Depends(provide_dependency)]):
    try:
        yield dep
    finally:
        pass


app = FastAPI()


@app.get("/")
def route(dep: Annotated[SomeDependency, FDepends(context)]):
    return {"value": dep.value}

Discriminated unions

Discriminated unions are supported through Pydantic's Discriminator.

from typing import Annotated, Literal

from pydantic import BaseModel, Discriminator

from pydantic_di import Load


class FileStore(BaseModel):
    type: Literal["FILE"] = "FILE"
    path: str


class S3Store(BaseModel):
    type: Literal["S3"] = "S3"
    bucket: str


Store = Annotated[FileStore | S3Store, Discriminator("type")]

store = Load(Store)

Environment variables:

STORE_TYPE=S3
STORE_S3_BUCKET=my-bucket

Result:

S3Store(type="S3", bucket="my-bucket")

Flattened discriminator convention

For discriminated unions, the discriminator selects which nested branch is used.

DUMMY_STORE_TYPE=A
DUMMY_STORE_A_FOO=hello
DUMMY_STORE_A_NUM=42

This becomes:

{
    "type": "A",
    "foo": "hello",
    "num": 42,
}

Discriminated unions from files

File-backed object loaders support the same discriminated union types.

from typing import Annotated, Literal

from pydantic import BaseModel, Discriminator
from pydantic_di.loaders import ObjectLoaderYaml


class YamlRoleStore(BaseModel):
    type: Literal["YAML"] = "YAML"
    path: str


class NullRoleStore(BaseModel):
    type: Literal["NULL"] = "NULL"


RoleStore = Annotated[
    YamlRoleStore | NullRoleStore,
    Discriminator("type"),
]

role_store = ObjectLoaderYaml[RoleStore](
    path="role-store.yaml",
).load()
type: YAML
path: roles.yaml

If the file does not contain the discriminator field, provide default_discriminator_value.

role_store = ObjectLoaderYaml[RoleStore](
    path="role-store.yaml",
    default_discriminator_value="YAML",
).load()
path: roles.yaml

The loader injects the discriminator value before validation.

attrs support

attrs classes can be loaded by converting them into Pydantic-compatible models.

import attrs

from pydantic_di import Load
from pydantic_di.pydanticize import pydanticize_type


@attrs.define
class Settings:
    host: str = "localhost"
    port: int = 8080


SettingsModel = pydanticize_type(Settings)
settings = Load(SettingsModel)

attrs defaults and factories are preserved.

List support

Simple lists can be supplied as JSON strings.

from pydantic import BaseModel

from pydantic_di import Load


class Config(BaseModel):
    values: list[str]
CONFIG_VALUES='["A", "B", "C"]'

Result:

Config(values=["A", "B", "C"])

Planned recursive list environment convention

For recursive object loading, lists may also be represented as indexed environment variables.

Simple values:

CONFIG_VALUES_0=A
CONFIG_VALUES_1=B
CONFIG_VALUES_2=C

Equivalent JSON form:

CONFIG_VALUES='["A", "B", "C"]'

Lists of Pydantic models:

from typing import Annotated, Literal

from pydantic import BaseModel, Discriminator


class BlahItem(BaseModel):
    type: Literal["blah"] = "blah"
    label: str


class OtherItem(BaseModel):
    type: Literal["other"] = "other"
    label: str


Item = Annotated[BlahItem | OtherItem, Discriminator("type")]


class SomeObject(BaseModel):
    list_field: list[Item]

Environment variables:

SOME_OBJECT_LIST_FIELD_0_TYPE=blah
SOME_OBJECT_LIST_FIELD_0_BLAH_LABEL=first
SOME_OBJECT_LIST_FIELD_1_TYPE=other
SOME_OBJECT_LIST_FIELD_1_OTHER_LABEL=second

Expected result:

SomeObject(
    list_field=[
        BlahItem(type="blah", label="first"),
        OtherItem(type="other", label="second"),
    ]
)

This keeps backwards compatibility with the existing JSON form while allowing recursive, schema-aware environment unpacking.

Custom loaders

Create a custom loader by subclassing LoaderBase.

from typing import Any

from pydantic_di.loaders.base import LoaderBase


class MyLoader(LoaderBase[str]):
    key: str

    def load_raw(self) -> Any:
        return f"value-for-{self.key}"

Then use it directly:

loader = MyLoader[str](key="example")
value = loader.load()

Public API

from pydantic_di import (
    Depends,
    Load,
    inject,
    sentinel,
    pydanticize_data,
    pydanticize_type,
    pydanticize_object,
    cached_type_adapter,
    is_supported_by_pydantic,
)

Design notes

Load resolves immediately.

settings = Load(Settings)

Depends resolves lazily.

settings: Annotated[Settings, Depends(Settings)]

persist=True caches by load target or loaded type.

Depends(Settings, persist=True)

persist=False creates a fresh dependency each time.

Depends(Settings, persist=False)

Development

Run tests:

pytest

Run formatting and linting:

ruff check .
ruff format .

Compatibility goals

The package aims to keep existing behaviour stable:

  • Existing JSON list loading should continue to work.
  • Existing flat object env-var loading should continue to work.
  • Existing discriminator conventions should continue to work.
  • New recursive list loading should be additive.

Metadata

Release files for pydantic-di 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pydantic-di 0.3.2
File Size Uploaded
pydantic_di-0.3.2.tar.gz 25.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pydantic-di 0.3.2
File Interpreter ABI Platform
pydantic_di-0.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 58.4 kB

Release files / pydantic_di-0.3.2.tar.gz

Download URL pydantic_di-0.3.2.tar.gz
Size 25.4 kB
Tags Source
SHA-256 checksum
How to use checksums
2a6756c34cdf1c2aa6968aba663e66ec4f94384e38ff0c25be1a7da87223ef7c
BLAKE2b-256 checksum
How to use checksums
813f4ac072741dfbc70f46cef0e0fd3f6f05e718e5a63a43fc7f84c0d7cf425e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pydantic_di-0.3.2-py3-none-any.whl

Download URL pydantic_di-0.3.2-py3-none-any.whl
Size 33.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ba90a8627c6b19962f9bacee3ab0f19b1758775b8c1d79c5c224c07274344d37
BLAKE2b-256 checksum
How to use checksums
167093163bb914d35f0dbef5612337188f51563c8273f8f503507afd17e7d99b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.3.2 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page