C
Chris Maillefaud
Guest
Getting started with environment variables in Python can feel overwhelming. You may juggle multiple .env files, try to keep secrets out of version control, and write repetitive code to parse types. Stela turns that chaos into a smooth, predictable workflow by offering:
Whether youβre building a small script or a large web service, Stela makes configuration clean, safe, and maintainable.
Environment variables let you keep configuration out of your codebase. Instead of hard-coding API URLs, database credentials, or feature flags, you store them externally and load them at runtime. This approach:
Yet most libraries leave you you to write the same boilerplate: read files, parse strings into ints or booleans, and override defaults. Stela automates all of that.
Stela divides configuration into two concepts:
It loads files in a well-defined order, casts strings to Python types automatically, and lets you add an optional final loader to pull values from other sources (AWS Parameter Store, HashiCorp Vault, etc.).
Install via pip:
Run the built-in init command:
This creates a set of configuration files and updates your
Try this quick test to observe precedence:
On Windows PowerShell:
By default Stela reads dotenv files in this order:
If you set STELA_ENV (for example
When the same key exists in multiple places, precedence (what wins) is:
This lets you:
In your Python code, just import and use:
Stela reads your
Stela parses values into native Python types automatically:
Stela handles JSON, booleans, numbers, lists, and dictionaries β no manual casting required.
Create files like
Switch environments by setting STELA_ENV:
Your code stays the same β Stela picks values based on
The
Use
Stela doesnβt only read dotenv files. You can register an optional final loader in your .stela config:
Then implement
Use Stela in your app as usual:
On startup, Stela loads your dotenv files, then calls the custom loader and merges its returned values into the loaded data. Values already present in
Don't want automatic type inference? Prefer a different file format? Define a default environment? Disable logs? Stela is flexible β check https://megalus.github.io/stela/ for all customization options.
Stela brings structure, safety, and simplicity to environment variable management in Python. You get:
Ready to try it? Visit the docs at https://megalus.github.io/stela/ and start cleaning up your configuration today.
If this helped or you have questions, please leave a comment below β I'm happy to answer.
Happy coding!
Continue reading...
- Automatic type inference
- A clear separation between settings and secrets
- Environment-specific .env files
- A simple, consistent API
- Extensible support for custom loaders
Whether youβre building a small script or a large web service, Stela makes configuration clean, safe, and maintainable.
Why environment variables matter
Environment variables let you keep configuration out of your codebase. Instead of hard-coding API URLs, database credentials, or feature flags, you store them externally and load them at runtime. This approach:
- Keeps secrets out of your Git history
- Makes it easy to switch configs for development, testing, and production
- Simplifies deployment to containers, CI pipelines, and cloud services
Yet most libraries leave you you to write the same boilerplate: read files, parse strings into ints or booleans, and override defaults. Stela automates all of that.
Introducing Stela
Stela divides configuration into two concepts:
- Settings: Non-sensitive values you can commit (API endpoints, timeouts, etc.)
- Secrets: Sensitive values you must keep out of your repo (passwords, tokens, etc.)
It loads files in a well-defined order, casts strings to Python types automatically, and lets you add an optional final loader to pull values from other sources (AWS Parameter Store, HashiCorp Vault, etc.).
Installing Stela
Install via pip:
Code:
pip install stela
Quick start: initialize your project
Run the built-in init command:
Code:
stela init --default
This creates a set of configuration files and updates your
.gitignore
. Typical files are:.env
β default settings (committed).env.local
β secrets (ignored).stela
β Stela configuration
Try this quick test to observe precedence:
- Add or uncomment a
MY_SECRET
line in.env
, then open a Python REPL and run:
Code:
from stela import env
print(env.MY_SECRET)
- Stop the REPL. Add or uncomment
MY_SECRET
in.env.local
, restart the REPL and run the same code β the value from.env.local
should take precedence over.env
. - Set
MY_SECRET
in your process environment and run the REPL again. On macOS/Linux:
Code:
export MY_SECRET="value_from_memory"
python -c "from stela import env; print(env.MY_SECRET)"
On Windows PowerShell:
Code:
$env:MY_SECRET="value_from_memory"
python -c "from stela import env; print(env.MY_SECRET)"
Understanding your dotenv files and precedence
By default Stela reads dotenv files in this order:
.env
.env.local
If you set STELA_ENV (for example
STELA_ENV=development
), Stela will also look for:.env.development
.env.development.local
When the same key exists in multiple places, precedence (what wins) is:
- System environment variable already set in memory (
os.environ
) β always wins. .env.{environment}.local
(if STELA_ENV is set).env.{environment}
(if STELA_ENV is set).env.local
.env
- If a value is not found anywhere, Stela raises a
StelaValueError
by default (this is configurable).
This lets you:
- Keep safe defaults in
.env
- Override with real secrets in
.env.local
- Customize per-environment values without changing defaults
- Still override anything at runtime via process envs (Docker, CI, shell) without editing files
Accessing settings and secrets
In your Python code, just import and use:
Code:
from stela import env
API_URL = env.API_URL # str
TIMEOUT = env.TIMEOUT # int
FEATURE_FLAG = env.FEATURE_FLAG # bool
DB_URL = env.DB_URL # str (may come from secrets if overridden)
Stela reads your
.env
files under the hood and exposes a single env
object.Type inference out of the box
Stela parses values into native Python types automatically:
Code:
# .env
PORT=8000
DEBUG=true
RETRY_TIMES=3
PI=3.14159
FEATURES=["search","login","signup"]
EXTRA_SETTINGS={"cache":true,"timeout":30}
Code:
from stela import env
assert isinstance(env.PORT, int)
assert isinstance(env.DEBUG, bool)
assert isinstance(env.PI, float)
assert isinstance(env.FEATURES, list)
assert isinstance(env.EXTRA_SETTINGS, dict)
Stela handles JSON, booleans, numbers, lists, and dictionaries β no manual casting required.
Managing multiple environments
Create files like
.env.testing
or .env.production
:
Code:
# .env.production
API_URL="https://api.example.com"
Switch environments by setting STELA_ENV:
Code:
export STELA_ENV=production
Your code stays the same β Stela picks values based on
STELA_ENV
automatically.Separating settings from secrets
The
stela init
command updates your .gitignore so:.env
is committed.env.local
and.env.*.local
are ignored
Use
.env
for harmless defaults and .env.local
for real credentials. This keeps secrets out of your repo while making it easy for teammates to get started.Advanced: custom final loader
Stela doesnβt only read dotenv files. You can register an optional final loader in your .stela config:
Code:
[stela]
final_loader = "myproject.loaders.custom_loader"
Then implement
myproject/loaders.py
:
Code:
# myproject/loaders.py
from typing import Any
from stela.config import StelaOptions
def custom_loader(options: StelaOptions, env_data: dict[str, Any]) -> dict[str, Any]:
"""Load settings from a custom source and merge into env_data.
Args:
options: Stela configuration options (includes current_environment).
env_data: Data already loaded from dotenv files.
Returns:
Updated data dictionary.
"""
# Example: pretend we fetched data from an external source
external = {"API_TIMEOUT": "5", "FEATURE_FLAG": "true"}
# Merge/override values from the external source into env_data
env_data.update(external)
return env_data
Use Stela in your app as usual:
Code:
from stela import env
# Values can come from dotenv files or your custom source.
# If a key is already set in os.environ at runtime, that in-memory value wins.
API_URL = env.API_URL
DB_PASSWORD = env.DB_PASSWORD
API_TIMEOUT = env.API_TIMEOUT # From custom loader
On startup, Stela loads your dotenv files, then calls the custom loader and merges its returned values into the loaded data. Values already present in
os.environ
are never overwritten.Extensibility
Don't want automatic type inference? Prefer a different file format? Define a default environment? Disable logs? Stela is flexible β check https://megalus.github.io/stela/ for all customization options.
Conclusion & next steps
Stela brings structure, safety, and simplicity to environment variable management in Python. You get:
- Zero-boilerplate type inference
- Clear separation of settings and secrets
- Straightforward multi-environment support
- Extensible custom loaders
Ready to try it? Visit the docs at https://megalus.github.io/stela/ and start cleaning up your configuration today.
If this helped or you have questions, please leave a comment below β I'm happy to answer.
Happy coding!

Continue reading...