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
Last updated: 24 September 2026
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):
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)staging None fallback
True 5Two 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:
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)Settings(debug=True, max_workers=4)
missing required environment variables: DATABASE_URLThree things are doing work here:
- Fail fast. The error names every missing variable, before the application serves a single request.
- Typed values.
debugis a real boolean andmax_workersa 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
settingsor 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):
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"])sk-from-server 8Pass 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:
# .gitignore
.env
# .env.example (committed)
DATABASE_URL=postgresql://user:password@localhost/dbname
API_KEY=replace-me
DEBUG=falseProduction: 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
ENVvalues 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=Falsepattern 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:
- 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.
- Check the provider's logs for use of the key since it leaked.
- 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?
Environment variables or a config file?
Are environment variables themselves secure?
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.
.envin.gitignore;.env.examplecommitted.- 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
- os.environ and os.getenv — Python documentation
- The Twelve-Factor App: Config — The Twelve-Factor App
- python-dotenv documentation — python-dotenv
- Build secrets — Docker documentation
- About secret scanning — GitHub documentation
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.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.