Store secrets in environment variables

Learn how to store secrets in environment variables to avoid hardcoding credentials in your code. This lesson covers the core concept, step-by-step implementation, best practices, and common pitfalls, with a hands-on exercise to reinforce learning.

Focus: store secrets in environment variables

Sponsored

Every developer has committed the sin: pushing an API key to GitHub, hardcoding a database password, or pasting a secret into a config file that ends up in a public repo. The pain is real — leaked credentials cost companies millions, get you blamed in incident reviews, and are a leading cause of data breaches. But there's a simple, battle-tested solution that starts with one habit: store secrets in environment variables instead of in your source code. In this lesson, you'll learn how to do this properly in Python, avoid the most common pitfalls, and prepare for the next step in your secure development journey.

The problem this lesson solves

Hardcoded secrets are a ticking time bomb. When you embed an API key or database password directly in your Python file, you create several serious risks:

  • Accidental exposure: One git push and your secret is forever in the history — even if you delete it later, it's already public.
  • Difficult rotation: If a secret leaks, you have to change it in every file that hardcodes it.
  • No separation of environments: Your local dev DB password is different from production, but hardcoded values force you into error-prone copy-paste.
  • Audit failure: Security scanners (like GitGuardian or trufflehog) will flag any repo with hardcoded secrets, damaging your team's trust.

Environment variables solve these problems by letting you inject secrets at runtime, outside your codebase. The secret lives in the process's environment — not in your files. This lesson shows you how to implement this pattern in Python, step by step.

Core concept / mental model

Think of your application as a stage actor. The script (your code) should never contain the actor's personal diary (secrets). Instead, the stage manager (the operating system) hands the actor their props right before the performance. In programming terms, environment variables are the props — set by the OS or a configuration service — and your code reads them when it starts.

A useful analogy: a configuration file is like a sticky note on your desk — anyone walking by can read it. An environment variable is like a sealed envelope given to you just before you need it; only the process that has the key can open it.

In Python, you access these variables through the os.environ dictionary or the safer os.getenv() function. The os.getenv() method returns None by default if the variable is missing, which lets you fail gracefully or with a clear error.

Here's the mental model: never put secrets in code or version-controlled files; always inject them at runtime.

How it works step by step

Setting and reading environment variables is straightforward, but doing it securely requires a few deliberate steps.

1. Set the environment variable in your shell

Before running your Python script, you set the variable in your terminal. This is temporary for that shell session.

# For Linux/macOS
export DATABASE_PASSWORD='s3cr3t!'

# For Windows (Command Prompt)
set DATABASE_PASSWORD=s3cr3t!

To make it permanent, you'd add the export line to your shell profile (e.g., .bashrc), but for development, a local .env file is more practical (see hands-on).

2. Read the variable in Python

Use os.getenv() to fetch the value. This is your secure access point.

import os

db_password = os.getenv('DATABASE_PASSWORD')
if db_password is None:
    raise RuntimeError('DATABASE_PASSWORD is not set!')

print('Connecting to database with password from env var')
# Do NOT print the password itself!

3. Keep .env files out of version control

For local development, you'll often use a .env file, but you must add it to .gitignore so you never commit it. The python-dotenv library loads it into the environment for you.

4. Use a secret manager in production

In production, you won't manually set environment variables. Instead, use a secrets manager like AWS Secrets Manager, Vault, or environment injection from your platform (Heroku, Docker, Kubernetes) — the principle remains the same: your code never sees a hardcoded secret.

Hands-on walkthrough

Let's put it into practice with a complete example. We'll build a tiny script that reads a database password from an environment variable and uses it to connect (simulated). We'll also use python-dotenv for local development.

Step 1: Set up a local environment file

Create a .env file in your project directory (never commit it).

# .env (local only)
DATABASE_PASSWORD=local_dev_pw_42
API_SECRET_KEY=sk_test_abc123

Step 2: Install python-dotenv

pip install python-dotenv

Step 3: Write the Python script

# app.py
import os
from dotenv import load_dotenv

# Load .env file into environment (only for local dev)
load_dotenv()

def get_secret(name: str) -> str:
    """Fetch a secret from environment, fail fast if missing."""
    value = os.getenv(name)
    if not value:
        raise RuntimeError(f'Missing required environment variable: {name}')
    return value

# In production, DATABASE_PASSWORD is injected by the platform
db_password = get_secret('DATABASE_PASSWORD')
api_key = get_secret('API_SECRET_KEY')

print('Connected to database (password loaded from env)')
print('API key loaded (but never printed!)')
# Use the secrets in API calls, not in logs

Run it:

python app.py

Expected output:

Connected to database (password loaded from env)
API key loaded (but never printed!)

Step 4: Protect yourself with .gitignore

Add .env to your .gitignore file:

.env

Verify that the file is ignored:

git status

Now you can safely commit your code without leaking secrets.

Step 5: Simulate production behavior

In a deployment, you might not have a .env file. If you run the script without the variable set, your get_secret function raises an error instead of failing silently — good for debugging.

Compare options / when to choose what

Environment variables are the baseline, but not the only solution. Here's how they compare with alternatives.

Method Pros Cons Best for
Hardcoding Dead simple Leaks immediately, no rotation Never — avoid
Config files (e.g., .ini, YAML) Structured Easy to commit accidentally, not encrypted at rest Legacy apps with no better option
Environment variables Simple, ubiquitous, built-in, not in code If process is compromised, env is visible; no built-in encryption Most applications, especially 12-factor apps
Secret managers (Vault, AWS Secrets Manager) Centralized, audited, encrypted, auto-rotation More setup and cost; requires network access Production systems, microservices, compliance-heavy environments

When to choose what:

  • For small projects and local dev: environment variables (with .env for convenience) are perfect.
  • For production with a single server: environment variables injected by your host (e.g., Heroku config vars) are solid.
  • For complex systems with dynamic secrets, multiple services, or strict compliance: invest in a secret manager — but your code still reads them via environment variables or API calls, so the habit you learn here still applies.

Pro tip: Even with secret managers, the API key you get is often injected as an environment variable into your process — so this lesson is foundational.

Troubleshooting & edge cases

You'll hit some common issues. Here's how to fix them.

Issue 1: Environment variable is None or empty

If os.getenv() returns None, either the variable is not set or your .env file isn't loaded. Check:

  • Did you call load_dotenv() before reading? Order matters.
  • Use a debug print (temporarily) to see what os.environ contains.

Issue 2: .env file is not loaded in production

Don't call load_dotenv() in production if your environment already has the variables. It will silently fail to find .env and your script still works, but it's wasted. Better to conditionally load based on ENVIRONMENT variable.

Issue 3: Special characters in secrets

If your password contains $, !, or spaces, shell quoting trips you up. Always use single quotes in export 'VAR=value' or better, use the .env file which handles special chars well with python-dotenv.

Issue 4: Committing .env by accident

If you already committed .env to git, remove it from tracking:

git rm --cached .env

Then add it to .gitignore. Note: the secret is already in history — rotate it immediately.

Issue 5: Env var set but not visible in Python

If you set the variable in one terminal, it won't appear in another terminal. Each shell session has its own environment. Set it in the same shell where you run python app.py.

Edge case: Variable name conventions

Use uppercase with underscores (DATABASE_PASSWORD) — it's a community standard and prevents confusion with code variables.

What you learned & what's next

You now understand the core concept of storing secrets in environment variables: the problem it solves (leaked secrets), the mental model (runtime injection), and the practical steps to implement it in Python. You can read secrets with os.getenv(), guard against missing values, and protect .env files with .gitignore. You also know when to level up to a secret manager.

Your learning objectives met:

  • You can explain why environment variables are a security best practice.
  • You completed a hands-on exercise that reads secrets from the environment and fails safely if missing.

Next in the Secure development track, you'll learn how to handle secrets in CI/CD pipelines — ensuring that your build and test processes also avoid hardcoding credentials, and how to integrate secret scanning into your workflow to catch leaks before they happen.

Practice recap

Take the script from the hands-on walkthrough and modify it to connect to a real database (or a mock). Create a .env file with a fake password, run the script, then try running it without the variable set. Observe how the error message helps you debug. As a bonus, set the environment variable manually in your shell and confirm the script works without the .env file — this simulates a production deployment.

Common mistakes

  • Committing the .env file to GitHub — always add it to .gitignore and verify with git status before pushing.
  • Printing the secret in logs or error messages — never output the actual value of a secret variable.
  • Using os.environ indexing (os.environ['KEY']) which raises KeyError — prefer os.getenv() for safer handling.
  • Setting the environment variable in one shell and expecting it in another — each terminal session has its own environment.
  • Hardcoding secrets as defaults in os.getenv('KEY', 'default_secret') — that defeats the purpose; fail fast instead.

Variations

  1. Use python-dotenv with a .env file for local development, but never commit it.
  2. For production, use a secrets manager like AWS Secrets Manager or HashiCorp Vault, and inject the secret as an environment variable when the app starts.
  3. On Windows, use set VAR=value in Command Prompt or $env:VAR='value' in PowerShell instead of export.

Real-world use cases

  • A Django web app reads its SECRET_KEY and database credentials from environment variables set in a CI/CD pipeline, ensuring zero secrets in version control.
  • A Python script running as a cron job pulls its SMTP password from an environment variable injected by systemd, avoiding a config file in the repo.
  • A microservice in Kubernetes reads its API key from a Secret object mounted as an environment variable, enabling rotation without redeploying code.

Key takeaways

  • The problem this lesson solves: hardcoded secrets lead to leaks, rotation nightmares, and security audit failures.
  • The mental model: secrets are injected at runtime via the process environment, never hardcoded in source.
  • Use os.getenv() to read secrets in Python, and always fail fast if a required variable is missing.
  • Keep .env files out of version control with .gitignore — this protects your local dev credentials.
  • For production, choose a secret manager for complex systems, but the principle of environment injection remains identical.
  • Troubleshoot common pitfalls: .env not loaded, special characters in secrets, and shell session environment isolation.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.