From e81bd4802844de5a5f2eb40d2d8bf79ac2f77f0b Mon Sep 17 00:00:00 2001 From: Dimas Putra Date: Thu, 11 Dec 2025 16:02:52 +0700 Subject: [PATCH 1/7] Add KeyringConfigSettingsSource for keyring integration --- .../sources/providers/keyring.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 pydantic_settings/sources/providers/keyring.py diff --git a/pydantic_settings/sources/providers/keyring.py b/pydantic_settings/sources/providers/keyring.py new file mode 100644 index 00000000..e05d4db7 --- /dev/null +++ b/pydantic_settings/sources/providers/keyring.py @@ -0,0 +1,56 @@ +from typing import TYPE_CHECKING, Mapping + +from pydantic_settings import BaseSettings, EnvSettingsSource + +if TYPE_CHECKING: + import keyring +else: + keyring = None + +def import_keyring(): + global keyring + if keyring is not None: + return + try: + import keyring + + return + except ImportError as e: + raise ImportError("Keyring is not installed, run `pip install keyring`") from e + + +class KeyringConfigSettingsSource(EnvSettingsSource): + def __init__( + self, + settings_cls: type[BaseSettings], + keyring_service: str, + case_sensitive: bool | None = True, + env_prefix: str | None = None, + ): + self.keyring_service = keyring_service + super().__init__( + settings_cls, case_sensitive=case_sensitive, env_prefix=env_prefix + ) + + def _load_env_vars(self) -> Mapping[str, str | None]: + import_keyring() + + env_vars = {} + for field in self.settings_cls.model_fields.keys(): + credential = keyring.get_credential( + self.keyring_service, self.env_prefix + field + ) + if credential is not None: + key = credential.username + key = key if self.case_sensitive else key.lower() + env_vars[key] = credential.password + + return env_vars + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(keyring_service={self.keyring_service})" + + +__all__ = [ + "KeyringConfigSettingsSource", +] From acdda74337c9dc7e48e58fe44d9b10653722601d Mon Sep 17 00:00:00 2001 From: Dimas Putra Date: Thu, 11 Dec 2025 16:06:08 +0700 Subject: [PATCH 2/7] Add KeyringConfigSettingsSource to providers --- pydantic_settings/sources/providers/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pydantic_settings/sources/providers/__init__.py b/pydantic_settings/sources/providers/__init__.py index 9be7a546..3dccb988 100644 --- a/pydantic_settings/sources/providers/__init__.py +++ b/pydantic_settings/sources/providers/__init__.py @@ -17,6 +17,7 @@ from .env import EnvSettingsSource from .gcp import GoogleSecretManagerSettingsSource from .json import JsonConfigSettingsSource +from .keyring import KeyringConfigSettingsSource from .pyproject import PyprojectTomlConfigSettingsSource from .secrets import SecretsSettingsSource from .toml import TomlConfigSettingsSource @@ -38,6 +39,7 @@ 'EnvSettingsSource', 'GoogleSecretManagerSettingsSource', 'JsonConfigSettingsSource', + 'KeyringConfigSettingsSource', 'PyprojectTomlConfigSettingsSource', 'SecretsSettingsSource', 'TomlConfigSettingsSource', From d7efbd14daf9a7939481089bf6a31f21fd9b943a Mon Sep 17 00:00:00 2001 From: Dimas Putra Date: Thu, 11 Dec 2025 16:07:32 +0700 Subject: [PATCH 3/7] Add KeyringConfigSettingsSource to sources --- pydantic_settings/sources/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pydantic_settings/sources/__init__.py b/pydantic_settings/sources/__init__.py index 3a557f7c..5098fca3 100644 --- a/pydantic_settings/sources/__init__.py +++ b/pydantic_settings/sources/__init__.py @@ -27,6 +27,7 @@ from .providers.env import EnvSettingsSource from .providers.gcp import GoogleSecretManagerSettingsSource from .providers.json import JsonConfigSettingsSource +from .providers.keyring import KeyringConfigSettingsSource from .providers.nested_secrets import NestedSecretsSettingsSource from .providers.pyproject import PyprojectTomlConfigSettingsSource from .providers.secrets import SecretsSettingsSource @@ -58,6 +59,7 @@ 'GoogleSecretManagerSettingsSource', 'InitSettingsSource', 'JsonConfigSettingsSource', + 'KeyringConfigSettingsSource', 'NestedSecretsSettingsSource', 'NoDecode', 'PathType', From f81f7a3359482824779c22716b2b037ce3e68851 Mon Sep 17 00:00:00 2001 From: Dimas Putra Date: Thu, 11 Dec 2025 16:10:53 +0700 Subject: [PATCH 4/7] Add KeyringConfigSettingsSource to imports --- pydantic_settings/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pydantic_settings/__init__.py b/pydantic_settings/__init__.py index 21124260..413868cf 100644 --- a/pydantic_settings/__init__.py +++ b/pydantic_settings/__init__.py @@ -20,6 +20,7 @@ GoogleSecretManagerSettingsSource, InitSettingsSource, JsonConfigSettingsSource, + KeyringConfigSettingsSource, NestedSecretsSettingsSource, NoDecode, PydanticBaseSettingsSource, @@ -53,6 +54,7 @@ 'GoogleSecretManagerSettingsSource', 'InitSettingsSource', 'JsonConfigSettingsSource', + 'KeyringConfigSettingsSource', 'NestedSecretsSettingsSource', 'NoDecode', 'PydanticBaseSettingsSource', From 1bf41db2feab0e13342e07aa0bee5765b35ed9a3 Mon Sep 17 00:00:00 2001 From: Dimas Putra Date: Thu, 11 Dec 2025 16:16:31 +0700 Subject: [PATCH 5/7] Add keyring dependency to pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f473b312..e0644fcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ aws-secrets-manager = ["boto3>=1.35.0", "boto3-stubs[secretsmanager]"] gcp-secret-manager = [ "google-cloud-secret-manager>=2.23.1", ] +keyring = ["keyring>=25.7.0"] [project.urls] Homepage = 'https://github.com/pydantic/pydantic-settings' From 4531f6d9193114b1d70f020290955ca931b29bc2 Mon Sep 17 00:00:00 2001 From: Dimas Putra Date: Fri, 12 Dec 2025 05:35:57 +0700 Subject: [PATCH 6/7] Change case sensitivity handling in keyring provider --- pydantic_settings/sources/providers/keyring.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pydantic_settings/sources/providers/keyring.py b/pydantic_settings/sources/providers/keyring.py index e05d4db7..3d1078dc 100644 --- a/pydantic_settings/sources/providers/keyring.py +++ b/pydantic_settings/sources/providers/keyring.py @@ -24,10 +24,12 @@ def __init__( self, settings_cls: type[BaseSettings], keyring_service: str, - case_sensitive: bool | None = True, + case_sensitive: bool | None = None, env_prefix: str | None = None, ): - self.keyring_service = keyring_service + self.keyring_service = ( + keyring_service if case_sensitive else keyring_service.lower() + ) super().__init__( settings_cls, case_sensitive=case_sensitive, env_prefix=env_prefix ) @@ -35,14 +37,18 @@ def __init__( def _load_env_vars(self) -> Mapping[str, str | None]: import_keyring() + prefix = self.env_prefix + if not self.case_sensitive: + prefix = self.env_prefix.lower() env_vars = {} for field in self.settings_cls.model_fields.keys(): - credential = keyring.get_credential( - self.keyring_service, self.env_prefix + field - ) + if not self.case_sensitive: + field = field.lower() + credential = keyring.get_credential(self.keyring_service, prefix + field) if credential is not None: key = credential.username - key = key if self.case_sensitive else key.lower() + if not self.case_sensitive: + key = key.lower() env_vars[key] = credential.password return env_vars From cbb9fe84b20140df2b64570b96e77500b0459b17 Mon Sep 17 00:00:00 2001 From: Dimas Putra Date: Fri, 12 Dec 2025 05:56:14 +0700 Subject: [PATCH 7/7] Document Keyring integration Added documentation for Keyring integration to securely load sensitive values. --- docs/index.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/index.md b/docs/index.md index f5ddb2ec..dd668ac1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2457,6 +2457,62 @@ For nested models, Secret Manager supports the `env_nested_delimiter` setting as For more details on creating and managing secrets in Google Cloud Secret Manager, see the [official Google Cloud documentation](https://cloud.google.com/secret-manager/docs). +## Keyring + +This integration allows you to securely load sensitive values such as passwords, API tokens, and other secrets directly from the system keyring. Instead of storing secrets in .env files or configuration files, you can fetch them at runtime using the operating system's secure credential store. + +The Keyring integration requires additional dependencies: + +```bash +pip install "pydantic-settings[keyring]" +``` + +There is one mandatory parameter: +- `keyring_service`: keyring service name + +Add `KeyringConfigSettingsSource` to your settings sources: + +```python +from pydantic import SecretStr +from pydantic_settings import ( + BaseSettings, + KeyringConfigSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, +) + +class Settings(BaseSettings): + secret_key: SecretStr + + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + keyring_settings = KeyringConfigSettingsSource( + settings_cls, + keyring_service=os.environ["KEYRING_SERVICE"], + ) + + return ( + init_settings, + env_settings, + dotenv_settings, + file_secret_settings, + keyring_settings, + ) +``` + +To set a secret, you can use `keyring set ` in your terminal: + +```bash +$ keyring set myapp secret_key +``` + ## Other settings source Other settings sources are available for common configuration files: