A Python library for managing hierarchical configuration files with profile-based inheritance and variable interpolation
Project description
Profile Config
Profile-based configuration management for Python applications.
What It Does
Profile Config manages application configuration using profiles (e.g., development, staging, production). It discovers configuration files in your project hierarchy, merges them with proper precedence, and resolves the requested profile.
Configuration Flow
1. Discovery Phase
Search locations (highest to lowest precedence):
./myapp/config.yaml <- Current directory
../myapp/config.yaml <- Parent directory
../../myapp/config.yaml <- Grandparent directory
~/myapp/config.yaml <- Home directory
2. Merge Phase
Files are merged with closer files taking precedence
3. Profile Resolution
defaults + profile + inherited profiles
4. Override Phase
Apply runtime overrides (highest precedence)
5. Interpolation Phase
Resolve ${variable} references
Example
Given this configuration file at myapp/config.yaml:
defaults:
host: localhost
port: 5432
debug: false
profiles:
development:
debug: true
database: myapp_dev
production:
host: prod-db.example.com
database: myapp_prod
This code:
from profile_config import ProfileConfigResolver
resolver = ProfileConfigResolver("myapp", profile="development")
config = resolver.resolve()
Produces this configuration:
{
"host": "localhost", # from defaults
"port": 5432, # from defaults
"debug": True, # from development profile (overrides defaults)
"database": "myapp_dev" # from development profile
}
Installation
pip install profile-config
For TOML support on Python < 3.11:
pip install profile-config[toml]
Basic Usage
1. Create Configuration File
Create myapp/config.yaml in your project:
defaults:
timeout: 30
retries: 3
profiles:
development:
debug: true
log_level: DEBUG
production:
debug: false
log_level: WARNING
2. Load Configuration
from profile_config import ProfileConfigResolver
# Load development profile
resolver = ProfileConfigResolver("myapp", profile="development")
config = resolver.resolve()
# Access configuration values
print(config["timeout"]) # 30 (from defaults)
print(config["debug"]) # True (from development profile)
print(config["log_level"]) # DEBUG (from development profile)
Configuration File Discovery
Profile Config searches for configuration files by walking up the directory tree from the current working directory, then checking the home directory.
Search Pattern
Default pattern: {config_name}/{profile_filename}.{extension}
Examples:
myapp/config.yaml(default)myapp/settings.yaml(custom filename)myapp/app.json(custom filename)
Search Order (highest to lowest precedence)
Current directory: ./myapp/config.yaml
Parent directory: ../myapp/config.yaml
Grandparent directory: ../../myapp/config.yaml
...
Home directory: ~/myapp/config.yaml
File Extensions
Searches for files with these extensions (in order):
.yaml.yml.json.toml
Example Directory Structure
/home/user/projects/myapp/
├── backend/
│ └── myapp/
│ └── config.yaml <- Project-specific config
└── myapp/
└── config.yaml <- Shared config
/home/user/myapp/
└── config.yaml <- User-specific config
When running from /home/user/projects/myapp/backend/:
- Finds
./myapp/config.yaml(current directory) - Finds
../myapp/config.yaml(parent directory) - Finds
~/myapp/config.yaml(home directory) - Merges all three (current directory has highest precedence)
Custom Profile Filename
Use a different filename instead of config:
# Search for settings.yaml instead of config.yaml
resolver = ProfileConfigResolver(
"myapp",
profile="development",
profile_filename="settings"
)
This searches for:
./myapp/settings.yaml../myapp/settings.yaml~/myapp/settings.yaml
Use cases:
- Organization naming standards (e.g.,
settings.yaml) - Multiple configuration types in same directory
- Legacy system compatibility
- More descriptive names (e.g.,
database.yaml,api.yaml)
Configuration File Format
Structure
# Optional: specify which profile to use by default
default_profile: development
# Optional: values applied to all profiles
defaults:
timeout: 30
retries: 3
# Required: profile definitions
profiles:
development:
debug: true
database: myapp_dev
production:
debug: false
database: myapp_prod
Supported Formats
YAML
defaults:
host: localhost
port: 5432
profiles:
development:
debug: true
JSON
{
"defaults": {
"host": "localhost",
"port": 5432
},
"profiles": {
"development": {
"debug": true
}
}
}
TOML
[defaults]
host = "localhost"
port = 5432
[profiles.development]
debug = true
Using the Default Profile
The "default" profile has special behavior that makes it easy to use only the defaults section without defining an explicit profile.
Automatic Creation
When you request profile="default" and no "default" profile exists in your configuration, an empty profile is automatically created. This returns only the values from the defaults section.
defaults:
host: localhost
port: 5432
timeout: 30
profiles:
development:
debug: true
port: 3000
production:
host: prod-db.com
debug: false
# No explicit "default" profile defined in config
resolver = ProfileConfigResolver("myapp", profile="default")
config = resolver.resolve()
# Returns only defaults:
# {
# "host": "localhost",
# "port": 5432,
# "timeout": 30
# }
Explicit Default Profile
If you define an explicit "default" profile, it takes precedence over the auto-creation:
defaults:
host: localhost
port: 5432
timeout: 30
profiles:
default:
timeout: 60 # Override default timeout
custom: true # Add custom setting
development:
debug: true
resolver = ProfileConfigResolver("myapp", profile="default")
config = resolver.resolve()
# Returns defaults + default profile:
# {
# "host": "localhost",
# "port": 5432,
# "timeout": 60, # Overridden by default profile
# "custom": True # Added by default profile
# }
Use Cases
1. Base configuration without environment-specific overrides:
# Get base configuration only
resolver = ProfileConfigResolver("myapp", profile="default")
base_config = resolver.resolve()
2. Fallback when no specific profile is needed:
import os
# Use environment-specific profile if set, otherwise use defaults
env = os.environ.get("ENV", "default")
resolver = ProfileConfigResolver("myapp", profile=env)
config = resolver.resolve()
3. Testing with minimal configuration:
# Tests can use "default" profile for baseline behavior
def test_app_with_defaults():
resolver = ProfileConfigResolver("myapp", profile="default")
config = resolver.resolve()
# Test with minimal config
Profile Inheritance
Profiles can inherit from other profiles using the inherits key.
Example
profiles:
base:
host: localhost
timeout: 30
development:
inherits: base
debug: true
database: myapp_dev
staging:
inherits: development
debug: false
host: staging-db.example.com
Resolution Order
For profile staging:
- Start with
baseprofile - Merge
developmentprofile (overridesbase) - Merge
stagingprofile (overridesdevelopment)
Result:
{
"host": "staging-db.example.com", # from staging (overrides base)
"timeout": 30, # from base
"debug": False, # from staging (overrides development)
"database": "myapp_dev" # from development
}
Multi-Level Inheritance
profiles:
base:
timeout: 30
development:
inherits: base
debug: true
development-team1:
inherits: development
team_id: team1
development-team2:
inherits: development
team_id: team2
Circular inheritance is detected and raises an error.
Variable Interpolation
Use ${variable} syntax to reference other configuration values.
Example
defaults:
app_name: myapp
base_path: /opt/${app_name}
data_path: ${base_path}/data
log_path: ${base_path}/logs
profiles:
development:
base_path: /tmp/${app_name}
Resolution
For profile development:
{
"app_name": "myapp",
"base_path": "/tmp/myapp", # interpolated
"data_path": "/tmp/myapp/data", # interpolated
"log_path": "/tmp/myapp/logs" # interpolated
}
Variables are resolved after profile inheritance is complete.
Runtime Overrides
Apply configuration overrides at runtime with highest precedence.
Dictionary Override
resolver = ProfileConfigResolver(
"myapp",
profile="production",
overrides={"debug": True, "host": "test-db.example.com"}
)
config = resolver.resolve()
File Override
resolver = ProfileConfigResolver(
"myapp",
profile="production",
overrides="/path/to/overrides.yaml"
)
config = resolver.resolve()
Supported formats: .yaml, .yml, .json, .toml
List of Overrides
Apply multiple overrides in order (later overrides take precedence):
resolver = ProfileConfigResolver(
"myapp",
profile="production",
overrides=[
"/path/to/base-overrides.yaml",
{"debug": True},
"/path/to/final-overrides.json"
]
)
config = resolver.resolve()
Precedence Order
1. Discovered config files (lowest)
2. Profile resolution
3. Override 1
4. Override 2
5. Override N (highest)
Configuration Options
Customize Search Behavior
resolver = ProfileConfigResolver(
config_name="myapp",
profile="development",
extensions=["yaml", "json"], # Only search these formats
search_home=False, # Don't search home directory
)
Custom Profile Filename
# Use settings.yaml instead of config.yaml
resolver = ProfileConfigResolver(
"myapp",
profile="development",
profile_filename="settings"
)
Custom Inheritance Key
# Use 'extends' instead of 'inherits'
resolver = ProfileConfigResolver(
"myapp",
profile="development",
inherit_key="extends"
)
Disable Variable Interpolation
resolver = ProfileConfigResolver(
"myapp",
profile="development",
enable_interpolation=False
)
Utility Methods
List Available Profiles
resolver = ProfileConfigResolver("myapp")
profiles = resolver.list_profiles()
print(profiles) # ['development', 'staging', 'production']
Get Discovered Files
resolver = ProfileConfigResolver("myapp")
files = resolver.get_config_files()
for file_path in files:
print(file_path)
Error Handling
Profile Config raises specific exceptions for different error conditions.
Exception Types
from profile_config.exceptions import (
ConfigNotFoundError, # No configuration files found
ProfileNotFoundError, # Requested profile doesn't exist
CircularInheritanceError, # Circular inheritance detected
ConfigFormatError # Invalid configuration file format
)
Example
from profile_config import ProfileConfigResolver
from profile_config.exceptions import ProfileNotFoundError
try:
resolver = ProfileConfigResolver("myapp", profile="nonexistent")
config = resolver.resolve()
except ProfileNotFoundError as e:
print(f"Profile not found: {e}")
# Handle error (use default profile, exit, etc.)
Common Patterns
Environment-Based Configuration
import os
from profile_config import ProfileConfigResolver
env = os.environ.get("ENV", "development")
resolver = ProfileConfigResolver("myapp", profile=env)
config = resolver.resolve()
Team-Specific Configuration
profiles:
development:
debug: true
development-team1:
inherits: development
team_id: team1
endpoint: "https://team1.internal.com"
development-team2:
inherits: development
team_id: team2
endpoint: "https://team2.internal.com"
import os
from profile_config import ProfileConfigResolver
team = os.environ.get("TEAM_NAME", "")
env = os.environ.get("ENV", "development")
profile = f"{env}-{team}" if team else env
resolver = ProfileConfigResolver("myapp", profile=profile)
config = resolver.resolve()
Configuration with Secrets
Store secrets separately and merge at runtime:
import json
from pathlib import Path
from profile_config import ProfileConfigResolver
# Load base configuration
resolver = ProfileConfigResolver("myapp", profile="production")
config = resolver.resolve()
# Load secrets from secure location
secrets_file = Path("/etc/secrets/myapp.json")
if secrets_file.exists():
with open(secrets_file) as f:
secrets = json.load(f)
config.update(secrets)
Or use overrides:
resolver = ProfileConfigResolver(
"myapp",
profile="production",
overrides="/etc/secrets/myapp.json"
)
config = resolver.resolve()
Format Comparison
| Feature | YAML | JSON | TOML |
|---|---|---|---|
| Comments | Yes | No | Yes |
| Multi-line strings | Yes | Escaped only | Yes |
| Type safety | Inferred | Limited | Native types |
| Nesting | Natural | Natural | Verbose for deep nesting |
| Readability | High | Medium | High |
| Ecosystem | Mature | Universal | Growing |
When to Use Each Format
YAML: Complex nested configurations, human-edited files JSON: API integration, machine-generated configs, data exchange TOML: Application configuration with type safety, flat structures
Examples
The examples/ directory contains working examples:
basic_usage.py- Basic configuration and profile usageadvanced_profiles.py- Inheritance patterns and error handlingdefault_profile_usage.py- Default profile auto-creation and use casesweb_app_config.py- Web application configuration managementtoml_usage.py- TOML format features
Run examples:
cd examples
python basic_usage.py
Development
Setup
git clone https://github.com/bassmanitram/profile-config.git
cd profile-config
pip install -e ".[dev,toml]"
Run Tests
pytest
Run Tests with Coverage
pytest --cov=profile_config --cov-report=html
API Reference
ProfileConfigResolver
ProfileConfigResolver(
config_name: str,
profile: str = "default",
profile_filename: str = "config",
overrides: Optional[Union[Dict, PathLike, List[Union[Dict, PathLike]]]] = None,
extensions: Optional[List[str]] = None,
search_home: bool = True,
inherit_key: str = "inherits",
enable_interpolation: bool = True,
)
Parameters:
config_name: Name of configuration directory (e.g., "myapp")profile: Profile name to resolve (default: "default")profile_filename: Name of profile file without extension (default: "config")overrides: Override values (dict, file path, or list of dicts/paths)extensions: File extensions to search (default: ["yaml", "yml", "json", "toml"])search_home: Whether to search home directory (default: True)inherit_key: Key name for profile inheritance (default: "inherits")enable_interpolation: Whether to enable variable interpolation (default: True)
Methods:
resolve() -> Dict[str, Any]: Resolve and return configurationlist_profiles() -> List[str]: List available profilesget_config_files() -> List[Path]: Get discovered configuration files
License
MIT License. See LICENSE file for details.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md for guidelines.
Links
- GitHub: https://github.com/bassmanitram/profile-config
- PyPI: https://pypi.org/project/profile-config/
- Issues: https://github.com/bassmanitram/profile-config/issues
Changelog
See CHANGELOG.md for version history.
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
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 profile_config-1.2.0.tar.gz.
File metadata
- Download URL: profile_config-1.2.0.tar.gz
- Upload date:
- Size: 26.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c21f0808948120e6d85feea158e6ae0a1dea521aa7ad0521f0468b18f26d3905
|
|
| MD5 |
7b3303e87a1e19f5b797232cbe8de9a6
|
|
| BLAKE2b-256 |
bd817a31c7c07a93179abb51c4f6a3458f05c4d9557e446fb8b75e08ca40c130
|
Provenance
The following attestation bundles were made for profile_config-1.2.0.tar.gz:
Publisher:
release.yml on bassmanitram/profile-config
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
profile_config-1.2.0.tar.gz -
Subject digest:
c21f0808948120e6d85feea158e6ae0a1dea521aa7ad0521f0468b18f26d3905 - Sigstore transparency entry: 696907389
- Sigstore integration time:
-
Permalink:
bassmanitram/profile-config@dd83dfda31ff3b8cbe3a34dfb0c9af99f800dc53 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/bassmanitram
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dd83dfda31ff3b8cbe3a34dfb0c9af99f800dc53 -
Trigger Event:
push
-
Statement type:
File details
Details for the file profile_config-1.2.0-py3-none-any.whl.
File metadata
- Download URL: profile_config-1.2.0-py3-none-any.whl
- Upload date:
- Size: 27.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b1bde42f47fdbe53d1b5d9722d16c3478448831e9290e92f8f5652b903dba58f
|
|
| MD5 |
c4e0db5c58f9803682794bf3ff0702c2
|
|
| BLAKE2b-256 |
9704b15a9f07e80012114292ea0cfe5159632022466e0824b17a569880795279
|
Provenance
The following attestation bundles were made for profile_config-1.2.0-py3-none-any.whl:
Publisher:
release.yml on bassmanitram/profile-config
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
profile_config-1.2.0-py3-none-any.whl -
Subject digest:
b1bde42f47fdbe53d1b5d9722d16c3478448831e9290e92f8f5652b903dba58f - Sigstore transparency entry: 696907406
- Sigstore integration time:
-
Permalink:
bassmanitram/profile-config@dd83dfda31ff3b8cbe3a34dfb0c9af99f800dc53 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/bassmanitram
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dd83dfda31ff3b8cbe3a34dfb0c9af99f800dc53 -
Trigger Event:
push
-
Statement type: