Python Configuration, Logging, and Secrets Best Practices

Jun 26, 2026
40 min read

AI Insights

Powered by GPT-4o-mini

Verified Context: python-configuration-logging-and-secrets-best-practices
Quick Answer

Learn Python best practices for environment variables, .env.example, settings classes, logging setup, log levels, secret handling, config validation, and safe project defaults.

Quick Summary

Learn essential Python practices for configuration, logging, and managing secrets securely to enhance your application's reliability.

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:

text
.env.example
src/
  my_app/
    config.py
    logging_config.py

Commit .env.example. Do not commit .env.

Last verified: June 26, 2026.

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.

python
import os

database_url = os.environ.get("DATABASE_URL", "sqlite:///local.db")

Use required variables when the app cannot run without them:

python
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:

text
DATABASE_URL=sqlite:///local.db
API_KEY=replace-with-local-secret
LOG_LEVEL=INFO

Do not commit it.

Commit .env.example:

text
DATABASE_URL=sqlite:///local.db
API_KEY=
LOG_LEVEL=INFO

This documents what must be configured.

4. Add .env To .gitignore

gitignore
.env
.env.local
.env.*.local

If 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:

python
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:

python
settings = load_settings()

6. Validate Required Secrets

Some settings should not have fake defaults.

python
def require_env(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"Missing required environment variable: {name}")
    return value

Use:

python
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:

python
# service.py
API_KEY = os.environ["API_KEY"]

This can break tests just by importing the module.

Better:

python
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.

python
import logging

logger = logging.getLogger(__name__)

def sync_orders():
    logger.info("Starting order sync")

Configure logging once at the application entry point:

python
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)

Do not call basicConfig in every module.

9. Log Levels

LevelUse For
DEBUGdetailed developer-only diagnostics
INFOnormal lifecycle events
WARNINGrecoverable problems
ERRORfailed operations
CRITICALprocess-level failures

Example:

python
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:

python
try:
    import_orders()
except ValueError:
    logger.exception("Order import failed")
    raise

logger.exception includes the traceback.

Use it only inside except blocks.

11. Do Not Log Secrets

Bad:

python
logger.info("Using API key %s", api_key)

Better:

python
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

python
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:

python
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:

python
logger.info("Processed file", extra={"file_name": file_name})

14. Use Safe Defaults

Good defaults:

  • local SQLite database
  • local API URL
  • INFO log 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.

python
timeout = settings.request_timeout_seconds

Then pass it to your HTTP client.

Timeouts are configuration because local, CI, and production environments may need different values.

16. Test Configuration

Example tests:

python
def test_load_settings_defaults(monkeypatch):
    monkeypatch.delenv("DATABASE_URL", raising=False)
    settings = load_settings()
    assert settings.database_url == "sqlite:///local.db"
python
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:

  • .env is ignored
  • .env.example is 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.

Frequently Asked Questions

Why should configuration be kept outside of code?
Configuration should describe environment-specific values, such as database URLs and API base URLs, to avoid hard-coding production values into Python files.
What is the purpose of a .env.example file?
A .env.example file documents what must be configured by providing example values, helping developers know which environment variables are required.
How should environment variables be read in Python?
Environment variables should be read using os.environ, and it's recommended to centralize this in a config.py file to avoid reading them in many files.
What should you do if a secret is accidentally committed to Git?
If a secret is committed, removing it from the next commit is not enough; you must rotate the secret to ensure security.
How can you ensure that required environment variables are set?
You can ensure required environment variables are set by using a function like require_env, which raises an error if the variable is missing.

Related Work

See how this thinking shows up in shipped systems.