Environment Variables and Secrets in Python Apps

A practical setup for configuration and secrets in Python applications: reading typed settings from environment variables and failing fast when one is missing, .env files that never reach Git, keeping secrets out of logs and Docker images, and what to do when a key leaks anyway.

GloryTecks Team

Training & Placement Faculty

6 May 2026 6 min read

Last updated: 24 September 2026

Share:

The fastest way to leak a database password is to write it in a Python file. The file gets committed, the repository gets shared or made public, and the password is now in the Git history for good. The fix is a habit, not a tool: code reads configuration from its environment; it never contains it. The Twelve-Factor methodology puts it as a test: could you open-source the code right now without exposing any credentials (The Twelve-Factor App: config)?

Reading environment variables

Every process has a set of environment variables, which Python exposes as os.environ, a dictionary-like mapping of strings (Python docs: os.environ):

python
import os

os.environ["APP_MODE"] = "staging"          # normally set outside Python
print(os.environ["APP_MODE"], os.getenv("NOT_SET"), os.getenv("NOT_SET", "fallback"))
print(bool("false"), int("4") + 1)
text
staging None fallback
True 5

Two details in that output cause most configuration bugs. os.environ["NAME"] raises KeyError when the variable is missing, while os.getenv() quietly returns None or your default. And every value is a string: bool("false") is True, because any non-empty string is truthy. Parse values deliberately.

One settings object, loaded once, checked at startup

Scattering os.getenv() calls across a codebase means a missing variable is discovered at 2 a.m., in the one code path that needs it. Read everything once, at startup, into a typed object, and refuse to start if something required is missing:

python
from dataclasses import dataclass, field

class ConfigError(RuntimeError):
    pass

TRUE = {"1", "true", "yes", "on"}

@dataclass(frozen=True)
class Settings:
    database_url: str = field(repr=False)     # never printed
    api_key: str = field(repr=False)
    debug: bool = False
    max_workers: int = 4

def load_settings(env=os.environ) -> Settings:
    missing = [name for name in ("DATABASE_URL", "API_KEY") if not env.get(name)]
    if missing:
        raise ConfigError("missing required environment variables: " + ", ".join(missing))
    return Settings(
        database_url=env["DATABASE_URL"],
        api_key=env["API_KEY"],
        debug=env.get("DEBUG", "false").strip().lower() in TRUE,
        max_workers=int(env.get("MAX_WORKERS", "4")),
    )

settings = load_settings({"DATABASE_URL": "postgresql://app:s3cret@db/sales", "API_KEY": "sk-test-123", "DEBUG": "true"})
print(settings)

try:
    load_settings({"API_KEY": "sk-test-123"})
except ConfigError as exc:
    print(exc)
text
Settings(debug=True, max_workers=4)
missing required environment variables: DATABASE_URL

Three things are doing work here:

  • Fail fast. The error names every missing variable, before the application serves a single request.
  • Typed values. debug is a real boolean and max_workers a real integer, parsed in one place.
  • field(repr=False) keeps the database URL and API key out of the object's printed form, so logging settings or an error that includes it does not leak them.

Passing env as a parameter also makes the loader trivial to test with a plain dictionary, as above. Libraries such as pydantic-settings build the same pattern from type hints if you prefer; Pydantic explained introduces the validation side.

.env files for local development

Typing ten export commands before every run is tedious, so local setups usually keep variables in a .env file and load it at startup with python-dotenv (python -m pip install python-dotenv). By default, load_dotenv() does not override variables that are already set, so the real environment of a server always wins over a stray file (python-dotenv documentation):

python
from pathlib import Path
from dotenv import load_dotenv

Path(".env").write_text("API_KEY=sk-from-dotenv\nMAX_WORKERS=8\n", encoding="utf-8")
os.environ["API_KEY"] = "sk-from-server"

load_dotenv(Path(".env"))
print(os.environ["API_KEY"], os.environ["MAX_WORKERS"])
text
sk-from-server 8

Pass the path explicitly, as here. Called with no argument, load_dotenv() looks for .env starting from the folder of the script that calls it and moving upwards, not from the folder you run the command in, so a file that sits elsewhere is silently not found.

The .env file holds real secrets, so it must never be committed. Commit a template instead, with names and harmless placeholders, so a new developer knows what to set:

text
# .gitignore
.env

# .env.example (committed)
DATABASE_URL=postgresql://user:password@localhost/dbname
API_KEY=replace-me
DEBUG=false

Production: where secrets should live

  • Use the platform's secret store. Cloud secret managers (AWS Secrets Manager, Azure Key Vault, Google Secret Manager), your CI system's encrypted secrets, or your host's environment settings inject values into the process at run time.
  • Keep secrets out of container images. Docker's documentation is explicit that build arguments and ENV values are unsuitable for secrets because they persist in the final image, where anyone who can pull it can read them (Docker: build secrets). Pass secrets when the container starts, not when it is built.
  • Keep them out of logs and error pages. Log the fact that a key was loaded, never the key. The repr=False pattern above helps; so does a logging setup that never dumps whole configuration objects (a practical Python logging setup).
  • Use different credentials per environment, so a leaked development key cannot touch production data.

When a secret leaks anyway

It happens: a .env committed by accident, a key pasted into a support ticket. Treat it as compromised the moment it leaves your control:

  1. Revoke or rotate the credential first. Deleting the file in a new commit does not help; the value is still in the repository's history and in every clone.
  2. Check the provider's logs for use of the key since it leaked.
  3. Then clean up the history if you need to, and add the file to .gitignore.

GitHub's secret scanning detects many known credential formats in repositories and can block a push that contains one (GitHub docs: secret scanning). It is a safety net, not a substitute for the habits above.

How do I set an environment variable for one run?

On Linux and macOS, prefix the command: DEBUG=true python app.py. In Windows PowerShell, set it for the session with $env:DEBUG = "true", then run the program. Values set this way disappear when the terminal closes, which is exactly what you want for experiments.

Environment variables or a config file?

Both have a place. Non-secret settings that rarely change between environments, such as feature defaults, can live in a committed config file. Anything that differs per deployment, and every secret, belongs in the environment or a secret manager.

Are environment variables themselves secure?

They keep secrets out of code and repositories, which is the main risk. They are still readable by the process and by anyone who can inspect it, so production systems add a secret manager, short-lived credentials and least-privilege access.

Checklist

  • No credentials in code, notebooks, Dockerfiles or committed config files.
  • One settings loader, run at startup, that fails with a clear list of missing variables.
  • .env in .gitignore; .env.example committed.
  • Secrets hidden from repr, logs and error responses.
  • A written plan for rotating each credential.

Where the settings module sits in a project, next to the package and its tests, is covered in how to structure a Python project. Error handling around a failed start-up is in error handling in Python. The Python Programming course covers the building blocks used here: modules and packages, exception handling and logging.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course