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_valuesshould 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_di-0.3.0.tar.gz.
File metadata
- Download URL: pydantic_di-0.3.0.tar.gz
- Upload date:
- Size: 24.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b782f99885a258ed9b58ca5e6891e82084161027fbb3e985ba5409d2580e4c08
|
|
| MD5 |
dbc68bc2b0bce5f76245e1232d72358b
|
|
| BLAKE2b-256 |
654004ec8e76aa44d67e0c3b7762e23cc753c59044525d3f551f885fd0e7724a
|
File details
Details for the file pydantic_di-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pydantic_di-0.3.0-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ef7b460bb99d64534ff83eadc2d46e127556eb72b5a366f6b9612f304d92bc5
|
|
| MD5 |
5e3392fc795ddbb8f93ca682cdfa2610
|
|
| BLAKE2b-256 |
ffcb24942ae5d67dd95ee1ed8f3986c1202bd08fc1a8a23cc279a2adf4ea381c
|