Python Configuration, Logging, and Secrets Best Practices
Quick Answer
Keep configuration outside code, keep secrets out of Git, and use logging instead of print debugging.
Minimum project files:
.env.example
src/
my_app/
config.py
logging_config.pyCommit .env.example. Do not commit .env.
Last verified: June 26, 2026.
Official Links
- Python
os.environ: https://docs.python.org/3/library/os.html#os.environ - Python logging: https://docs.python.org/3/library/logging.html
- Python dataclasses: https://docs.python.org/3/library/dataclasses.html
- Twelve-Factor App config principle: https://12factor.net/config
What You Will Learn
By the end, you should be able to:
- separate config from code
- use environment variables safely
- create
.env.example - avoid committing secrets
- centralize settings
- validate required config
- configure logging once
- choose log levels
- avoid leaking secrets in logs
1. Configuration vs Code
Code should describe behavior.
Configuration should describe environment-specific values.
Examples of configuration:
- database URL
- API base URL
- log level
- feature flags
- file paths
- timeout values
- credentials
Do not hard-code production values into Python files.
2. Use Environment Variables
Read environment variables with os.environ.
import os
database_url = os.environ.get("DATABASE_URL", "sqlite:///local.db")Use required variables when the app cannot run without them:
api_key = os.environ["API_KEY"]This raises KeyError if missing.
For user-friendly errors, validate settings in one place.
3. Commit .env.example
Local .env:
DATABASE_URL=sqlite:///local.db
API_KEY=replace-with-local-secret
LOG_LEVEL=INFODo not commit it.
Commit .env.example:
DATABASE_URL=sqlite:///local.db
API_KEY=
LOG_LEVEL=INFOThis documents what must be configured.
4. Add .env To .gitignore
.env
.env.local
.env.*.localIf a secret was committed, removing it from the next commit is not enough. Rotate the secret.
5. Centralize Settings
Avoid reading environment variables in many files.
Create config.py:
from dataclasses import dataclass
import os
@dataclass(frozen=True)
class Settings:
database_url: str
api_base_url: str
log_level: str = "INFO"
request_timeout_seconds: int = 30
def load_settings() -> Settings:
return Settings(
database_url=os.environ.get("DATABASE_URL", "sqlite:///local.db"),
api_base_url=os.environ.get("API_BASE_URL", "http://localhost:8000"),
log_level=os.environ.get("LOG_LEVEL", "INFO"),
request_timeout_seconds=int(os.environ.get("REQUEST_TIMEOUT_SECONDS", "30")),
)Use it:
settings = load_settings()6. Validate Required Secrets
Some settings should not have fake defaults.
def require_env(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return valueUse:
api_key = require_env("API_KEY")Fail early when required secrets are missing.
7. Avoid Global Configuration Surprises
Avoid code that reads environment variables at import time in many modules.
Weak:
# service.py
API_KEY = os.environ["API_KEY"]This can break tests just by importing the module.
Better:
def create_service(settings: Settings):
return Service(api_key=settings.api_key)Pass config into the code that needs it.
8. Logging Basics
Use Python's logging module.
import logging
logger = logging.getLogger(__name__)
def sync_orders():
logger.info("Starting order sync")Configure logging once at the application entry point:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)Do not call basicConfig in every module.
9. Log Levels
| Level | Use For |
|---|---|
DEBUG | detailed developer-only diagnostics |
INFO | normal lifecycle events |
WARNING | recoverable problems |
ERROR | failed operations |
CRITICAL | process-level failures |
Example:
logger.debug("Parsed %s rows", row_count)
logger.info("Import completed")
logger.warning("Skipped row with missing email")
logger.error("Could not write report")10. Use logger.exception
Inside an exception handler:
try:
import_orders()
except ValueError:
logger.exception("Order import failed")
raiselogger.exception includes the traceback.
Use it only inside except blocks.
11. Do Not Log Secrets
Bad:
logger.info("Using API key %s", api_key)Better:
logger.info("API key configured: %s", bool(api_key))Never log:
- API keys
- passwords
- access tokens
- refresh tokens
- private cookies
- full database URLs with passwords
12. Mask Sensitive Values
def mask_secret(value: str, visible: int = 4) -> str:
if not value:
return ""
if len(value) <= visible:
return "*" * len(value)
return "*" * (len(value) - visible) + value[-visible:]Use:
logger.info("Token suffix: %s", mask_secret(token))Only log masked values when it helps operations.
13. Structure Logs For Apps
For larger apps, JSON logs are useful.
Simple text logs are fine for beginner projects.
The important part is consistency:
- timestamp
- level
- module name
- message
- request ID or job ID when available
Example:
logger.info("Processed file", extra={"file_name": file_name})14. Use Safe Defaults
Good defaults:
- local SQLite database
- local API URL
INFOlog level- conservative timeout
Bad defaults:
- production database URL
- real secret key
- debug mode in production
- no timeout for network calls
Defaults should be safe for local development.
15. Add Timeouts To Network Config
Do not let network calls hang forever.
timeout = settings.request_timeout_secondsThen pass it to your HTTP client.
Timeouts are configuration because local, CI, and production environments may need different values.
16. Test Configuration
Example tests:
def test_load_settings_defaults(monkeypatch):
monkeypatch.delenv("DATABASE_URL", raising=False)
settings = load_settings()
assert settings.database_url == "sqlite:///local.db"import pytest
def test_require_env_fails_when_missing(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises(RuntimeError, match="API_KEY"):
require_env("API_KEY")Configuration bugs should be caught before deployment.
17. Configuration Checklist
Before sharing a Python project, confirm:
.envis ignored.env.exampleis committed- required settings are documented
- secrets are not hard-coded
- settings are centralized
- missing required config fails clearly
- logging is configured once
- logs do not expose secrets
- network timeouts are configured
- config has tests
18. FAQ
Should I commit .env?
No. Commit .env.example instead.
Where should I read environment variables?
Read them in one configuration module, then pass settings into the code that needs them.
Should beginners use Pydantic Settings?
For small projects, a dataclass is enough. For larger apps, Pydantic Settings is useful for validation and type conversion.
Is print okay?
Print is fine for tiny scripts. Use logging for apps, CLIs, background jobs, and anything you may need to debug later.
What if I already committed a secret?
Rotate the secret immediately. Deleting it from a later commit does not make the old secret safe.
