Environment Variables and Secrets
Part of Essentials
This is part of Essentials — core Python patterns for working platform engineers.
The Day One safety guide told you not to hardcode credentials. This article shows you the full pattern for doing it properly — reading from environment variables, falling back gracefully, validating at startup, and working with .env files.
Where You've Seen This
You already do this in bash:
And in CI/CD — GitHub Actions calls them "secrets", GitLab calls them "CI/CD variables", but the mechanism is the same: the value is injected into the environment at runtime. Your script reads $DB_PASSWORD without ever seeing where it came from.
Docker Compose uses .env files. Kubernetes uses Secrets mounted as environment variables. The pattern is universal. Python gives you a cleaner API for reading and validating them.
Reading from the Environment
| Reading environment variables | |
|---|---|
os.environ["API_KEY"]raisesKeyErrorif the variable isn't set. Use this for required variables — you want to fail immediately, not silently continue.os.environ.get("DB_PASSWORD")returnsNoneif the variable isn't set. Use for optional variables, or when you want to handle the missing case yourself.os.environ.get("LOG_LEVEL", "INFO")returns"INFO"ifLOG_LEVELisn't set. Use for variables that have a sensible default.
Fail Fast on Missing Required Credentials
A script that starts without its credentials and fails 30 steps later is painful to debug. Validate at startup:
Run this before you open database connections, before you import configuration, before you do anything. Missing credentials surface in the first second, not after a 45-second deploy sequence.
The .env File Pattern
For local development, setting export for every variable before running a script is tedious. A .env file is the standard solution:
Never commit .env to git. Add it to .gitignore immediately.
Parsing a .env File Without a Library
For simple cases, you don't need python-dotenv:
- Skip blank lines and comments (lines starting with
#). partition("=")splits on the first=only — handles values that contain=(like base64 tokens).- Strip surrounding quotes —
.envfiles often quote values, Python'sos.environdoesn't need them. - Don't override variables already set in the real environment. CI/CD secrets take precedence over
.env.
Using python-dotenv
If your project already has dependencies, python-dotenv is the standard library:
| python-dotenv usage | |
|---|---|
That's the full integration. load_dotenv() loads the file and populates os.environ. The rest of your code reads from os.environ normally — it doesn't need to know about python-dotenv.
Grouping Credentials Into a Config Object
As scripts grow, passing individual environment variables through function arguments gets messy. Group them:
@dataclassgenerates__init__,__repr__, and comparison methods automatically.config.api_keyinstead ofos.environ["API_KEY"]scattered through the codebase.
Now your functions take a config object. Credentials live in one place, are validated once at startup, and are never passed as strings through function arguments.
What Not to Log
| ❌ Logging secrets | |
|---|---|
| ✅ Log the intent, not the value | |
|---|---|
If your @dataclass has a __repr__, override it to mask sensitive fields:
| Masking secrets in repr | |
|---|---|
Practice Exercises
Exercise 1: Validate and report
Write a check_env() function that takes a list of required variable names and a list of optional variable names. It should print a summary of which required vars are set, which are missing, and which optional vars were found. Exit with a non-zero code only if required vars are missing.
Answer
Exercise 2: Mask a secret in output
Write a mask(value) function that returns the last 4 characters of a string with everything before it replaced by *. For example, mask("sk-abc123def456") returns "**********e456". This is useful for confirming a key was loaded without revealing it.
Quick Recap
| Pattern | When to Use |
|---|---|
os.environ["VAR"] |
Required variable — fail fast if missing |
os.environ.get("VAR") |
Optional variable — handle None yourself |
os.environ.get("VAR", "default") |
Variable with a sensible fallback |
load_dotenv() |
Local development with a .env file |
@dataclass + from_env() |
Group credentials for scripts with multiple vars |
| Fail-fast validation at startup | Surface missing credentials immediately |
What's Next
- Working with YAML — Reading, modifying, and generating Kubernetes manifests and other YAML configs
Further Reading
Official Documentation
os.environ— The environment variable mappingdataclasses—@dataclassdecorator reference
Libraries
python-dotenv— The standard.envfile librarypydantic-settings— For larger projects that need type validation and settings management