Environment Variables for Secrets
Learn to manage secrets with environment variables in Python for DevOps automation — practical steps, troubleshooting, and next steps.
Focus: manage secrets with environment variables
Your deployment script has the API key hard-coded, your database password is in the repo, and your CI logs are a secret goldmine. If that sounds familiar, you are one git push away from a breach that brings your infrastructure down. This lesson teaches you how to manage secrets with environment variables — the standard, practical way to keep credentials out of your code while still using them in your Python automation. By the end, you'll read secrets at runtime, keep them out of version control, and know what to do when things go wrong.
The problem this lesson solves
Hard-coding secrets like API tokens, database passwords, and SSH keys directly into Python scripts creates three serious problems:
- Secrets leak into version control. Once a secret is committed, it lives in your Git history forever, even if you remove it later. Attackers scan public repos for these patterns 24/7.
- Secrets are shared too widely. Everyone with access to the repo — including contractors, CI systems, and sometimes the public — gets the credentials.
- Rotating secrets is a nightmare. If you change a password, you must edit code, rebuild containers, and redeploy everything. That's slow and error-prone.
Environment variables solve all three by separating configuration from code. The code reads the value at runtime from the environment, not from a literal in the source file. This is a foundational practice in DevOps — think of it as the difference between writing your password on a sticky note on your monitor versus having a key-card reader at the door. The sticky note is visible to anyone who walks by; the key-card lets only authorized people in, and you can revoke it instantly.
Pro tip: Even if you're writing a quick one-off script, never put a real secret in the code. Bad habits form fast, and the one time you forget is the time it leaks.
Core concept / mental model
The mental model for managing secrets with environment variables is simple: your application is a function of both code (what it does) and environment (where it runs). Secrets belong to the environment — the machine, container, or CI runner — not to the codebase.
In Python, the os.environ dictionary holds all environment variables of the current process. It behaves like a regular dict, but reading a missing key raises a KeyError — which is actually a good thing, because it forces you to handle missing secrets deliberately.
Think of it this way: - Config (URLs, timeouts) is often non-sensitive and can have defaults. - Secrets (API keys, passwords, tokens) must never have defaults and must come from the environment.
The flow is straightforward:
Development → you set a secret in your shell or a .env file (which is not committed).
CI/CD → the pipeline injects secrets from its secure store (e.g., GitHub Actions secrets, GitLab variables).
Production → the container orchestrator or cloud provider provides the secret via environment variables.
Your Python code stays identical across all three environments — it just reads from os.environ.
How it works step by step
Let's walk through the lifecycle of a secret managed via environment variables:
- Store the secret — set it in your local shell (
export API_KEY=...), in a.envfile for development, or in your CI/CD platform's secure settings. - Load the secret in Python — use the
osmodule to access the value at runtime. - Use the secret — pass it to your HTTP client, database connection, or authentication function.
- Remove the secret when done — unset it or let the process exit; environment variables are per-process and vanish when the process ends.
- Never log the secret — avoid printing it or including it in error messages.
The os.getenv function is the most common way to read a variable, because you can provide a fallback. For secrets, though, you usually want to crash if the variable is missing, not silently use a default. So os.environ["SECRET"] is safer for required secrets.
You also need to manage environment variables in files like .env for local development. The python-dotenv library loads a .env file into os.environ at the start of your script, so you can keep the file out of Git.
Hands-on walkthrough
Let's see this in action with a real DevOps scenario: a script that queries an external API using a token.
Example 1: Reading a secret from the environment
import os
# Dangerous hard-coded way / NEVER do this
# API_KEY = "abc123secret"
# Safe way: read from environment
API_KEY = os.environ["API_KEY"] # raises KeyError if not set
print(f"API key loaded (length={len(API_KEY)})")
Run it with the variable set:
export API_KEY="sk-1234-abcd"
python script.py
Output:
API key loaded (length=13)
The script didn't print the key itself — a good habit.
Example 2: Using getenv with a default (for non-secret config)
import os
# For non-secret config, a default is fine
TIMEOUT = int(os.getenv("TIMEOUT", "30"))
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
# For secrets, require them explicitly
DATABASE_URL = os.environ["DATABASE_URL"]
print(f"Timeout: {TIMEOUT}s, Log level: {LOG_LEVEL}")
print(f"DB URL loaded (length={len(DATABASE_URL)})")
Set only the secret: export DATABASE_URL="postgres://user:pass@localhost:5432/mydb" and run — you'll see the defaults used.
Example 3: Loading from a .env file with python-dotenv
# pip install python-dotenv
from dotenv import load_dotenv
import os
# Loads .env into environment variables
load_dotenv()
# Now os.getenv / os.environ see the .env contents
API_KEY = os.getenv("API_KEY")
if not API_KEY:
raise SystemExit("API_KEY is not set. Create a .env file from .env.example.")
# Use the API key to make a request
import requests
resp = requests.get("https://api.example.com/data", headers={"Authorization": f"Bearer {API_KEY}"})
print(resp.status_code)
Create a .env file:
API_KEY=sk-1234
And a .gitignore entry:
.env
The output is just the HTTP status code — no secret revealed.
Example 4: Using a secrets helper module
# secrets.py
import os
from functools import lru_cache
@lru_cache()
def get_secret(name: str) -> str:
"""Return the value of a required environment variable."""
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
Now in your main script:
from secrets import get_secret
db_password = get_secret("DB_PASSWORD")
api_token = get_secret("API_TOKEN")
# Use them in your automation...
This caches the values so they're read only once — good for performance and consistency.
Pro tip: Keep your
.env.examplecommitted with placeholder variable names but no real values. It documents what secrets your app needs.
Compare options / when to choose what
You have several ways to manage secrets in Python. Let's compare them:
| Approach | Pros | Cons | Best for |
|---|---|---|---|
| Environment variables | Simple, universal, no extra dependencies | You must manage them outside code | Most DevOps scripts and containers |
.env files + python-dotenv |
Easy for local dev, groups secrets | Risk of committing if not ignored | Local development and small services |
| Secret managers (e.g., AWS Secrets Manager, Vault) | Centralized, rotation, access control | Requires setup and SDK, more complex | Production clusters, team collaboration |
| Hard-coded (never) | None | Leaks, rotation nightmare | Never |
When to choose what:
- For a quick script on your laptop: environment variables or a .env file.
- For a deployed service: environment variables injected by your orchestrator (Kubernetes secrets, Docker --env-file, etc.).
- For high-security environments: a dedicated secret manager with API access from Python.
Environment variables remain the baseline because they are language-agnostic, simple, and supported everywhere. Secret managers often export their values as environment variables to your processes anyway.
Troubleshooting & edge cases
Common issues you'll hit and how to fix them:
KeyErrorwhen reading a secret — The variable isn't set in the current shell or the process doesn't inherit it. Check yourexportstatement or container environment. Add a clear error message:raise SystemExit("Missing API_KEY").- Secret appears as
None— You usedos.getenvwithout a default, and the variable isn't set. Useos.environ.get("KEY")and check forNoneor empty string. .envfile not loading — The file isn't in the current working directory, or you forgotload_dotenv()at the top. Use an absolute path or ensure you're in the right directory..envaccidentally committed — Even if you delete it, it's in history. Use tools likegit-secretsortrufflehogto scan past commits, and rotate the leaked secret immediately.- Secrets showing in logs — Never print them. Use
len()to confirm presence, and configure your logging to filter out sensitive fields. - Special characters in values — Quotes and spaces in
.envvalues can break parsing. Use quotes in the file:SECRET="my secret", and use a library that handles it (python-dotenv does). - Environment variables not available in Docker — Use
--envor--env-filewhen running containers, or define them in your deployment manifest.
What you learned & what's next
You now know how to manage secrets with environment variables — the cornerstone of secure DevOps automation. You can read secrets from the environment, load them from .env files locally, and choose between environment variables and dedicated secret managers when needed. You also understand the pitfalls, like missing variables, committed .env files, and logging secrets.
This skill applies to every piece of automation you'll write: boto3 scripts that need AWS credentials, kubectl helpers that require cluster tokens, and CI pipelines that deploy your infrastructure. The next lesson in this track builds on this foundation — likely covering how to securely handle configuration files or integrate with a secret manager like AWS Secrets Manager for more advanced scenarios. With this knowledge, your automation is now both powerful and secure.
Practice recap
Create a small script that connects to a mock API using an API key. Store the key in a .env file, load it with python-dotenv, add .env to .gitignore, and verify it works when the file exists and crashes when it's removed. Then refactor to use a get_secret helper function.
Common mistakes
- Hard-coding secrets directly in source code or scripts
- Committing a
.envfile to version control without adding it to.gitignore - Using
os.getenvwith a default value for secrets (falls back to insecure value) - Printing or logging secrets, either intentionally or via exception messages
- Forgetting to load
.envin the correct working directory, leading toKeyError
Variations
- Use
dotenv_valuesto load.envinto a dict without modifyingos.environ - Use a secret manager like AWS Secrets Manager or HashiCorp Vault and fetch at runtime
- In CI/CD, inject secrets directly as environment variables from the platform (GitHub Actions, GitLab CI)
Real-world use cases
- CI pipeline deploying to AWS uses an access key stored as a CI secret, exposed to the Python deployment script via env var
- A Dockerized cron job reads a database password from the container's environment, set by
docker run --env - A Kubernetes pod authenticates to an external API using a token injected as an environment variable from a secret object
Key takeaways
- Always separate secrets from code by using environment variables
- Read secrets with
os.environ["NAME"]to fail fast when missing - Use
.envfiles and python-dotenv only for local development - Never commit
.envfiles or hard-code credentials - Choose a secret manager for production-grade security and rotation
- Never log secret values
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.