What Python projects usually do
A common development setup puts values in a project-local .env file and loads them with python-dotenv:
# .env
DATABASE_URL=postgresql://localhost/myapp
PAYMENTS_API_KEY=sk_test_...
from dotenv import load_dotenv
import os
load_dotenv()
database_url = os.environ["DATABASE_URL"]
This is ordinary Python practice. FastAPI documents the same pattern through Pydantic Settings, and Django recommends loading sensitive settings from environment variables or private files in its deployment checklist.
The problem is where the values live.
Adding .env to .gitignore helps prevent a commit. It does not encrypt the file or move it out of the project workspace.
Every developer still needs a plaintext copy. Editors, scripts, plugins and coding agents which can read the workspace may also be able to read it. When a value changes, the team has to find and replace each copy.
The Python code is not the problem. os.environ is a useful runtime interface. The project-local plaintext file is the part Kimen replaces.
Keep the Python code. Change how it starts.
This example assumes Python 3 and Kimen are installed. Create a minimal application which verifies the variable without printing its value:
# app.py
import os
database_url = os.environ["DATABASE_URL"]
print("DATABASE_URL is available")
Initialize the local encrypted vault once, if you do not already have one, then store the values:
kimen vault init
kimen secret set python_dev.database_url
kimen secret set python_dev.payments_api_key
Commit a profile containing names and vault references, not values:
# .kimen/profiles/dev.kmap
env DATABASE_URL=python_dev.database_url
env PAYMENTS_API_KEY=python_dev.payments_api_key
Validate the profile, then start Python through Kimen:
kimen doctor --profile dev
kimen run --profile dev -- python app.py
The second command should print:
DATABASE_URL is available
Kimen opens the vault, resolves the profile and gives the child process ordinary environment variables. The application can continue to call os.environ["DATABASE_URL"].
With no active session, both doctor and run ask for the vault passphrase. If you expect several invocations, start a short session with kimen session start --ttl 30m and finish with kimen session lock.
You can migrate without removing python-dotenv first.
By default, load_dotenv() uses override=False and does not replace an environment variable which already exists. That makes an incremental migration possible:
kimen run --profile dev -- python app.py
Kimen supplies the migrated names before Python starts. python-dotenv can continue loading any remaining local development values until they have also moved. Check projects which explicitly set override=True, because that reverses the precedence.
Once the project no longer needs .env, remove the call to load_dotenv() if it serves no other purpose. Framework settings code does not otherwise need to change.
The same rule applies to FastAPI, Django and notebooks.
FastAPI and Pydantic Settings
If a settings model reads environment variables, start the server through Kimen:
kimen run --profile dev -- fastapi dev main.py
Django
Keep secrets out of settings.py and start Django with the same profile:
kimen run --profile dev -- python manage.py runserver
IPython and Jupyter
Start the interactive process through Kimen instead of loading a project-local file after it starts:
kimen run --profile dev -- ipython
kimen run --profile dev -- jupyter lab
The notebook kernel receives the selected variables for its lifetime. Code executed in that kernel can read them, so only project values into notebooks and extensions you trust.
What this protects, and what it does not
- No secret value needs to be stored in the Python project.
- The committed profile documents required names without containing credentials.
- The secret is not exported into every later command in the parent shell.
- The Python process receives the value and can read or copy it.
- A long-running server, REPL or notebook holds the selected environment for its lifetime.