You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I think the first time I even heard of the likes of python-dotenv and python-decouple were in association with LLM code, usually as a way of establishing API keys and other secrets. This hasn't ever really sat well with me. I think the program should inertly read the info passed in through the environment. Over time I've run into enough strange bugs in diverse codebases from such mixing to convince me of that gut feel. I think one of the healthiest things the twelve-factor app methodology has brought along is this understanding of the utility of the classic UNIX environment, but let's do it right: config management sets the environment, and the code reads it. No more leaking across this boundary.
I do admit that properly setting the environment can be trickier than it seems at first glance. Differences across platforms, and even across program launcher and shell variants can make things tricky, but that's kind of the point—you tailor the environment set-up to the host environment, and all that matters is that the correct values get across to the program.
Common tools/approaches for setting the environment
Note: this guide is very Linux/BSD-biased, and all code snippets are tested on Mac OS X under the zsh shell.
Sample Python code, envtest.py:
importosprint(os.getenv('HELLO', 'NOT FOUND'))
If you run:
HELLO="Wide world"
python envtest.py
you'll get NOT FOUND because the environment variable set in the first command is not automatically passed into the subshell for the python process of the second command.
Modify the shell's environment so that it sticks for future commands
Note: you can always view an env var value using echo or printenv
printenv HELLO
You might be tempted to use echo:
echo$HELLO
But the latter will surprise you in several situations, so printenv as an easier to understand choice.
You'll want to be familiar with some of the implications of shell substitutions and escaping. For example, if you tried to use an exclamation point in the value:
export HELLO="Wide world!"
python envtest.py
You'd run into problems because ! demarcates shell history expansion. You can use a backslash escape:
export HELLO="Wide world\!"
python envtest.py
Same with $, which is used to demarcate shell variables.
export HELLO="Wide world\$"
python envtest.py
You can also use single quotes to avoid interpolation, but note that will prevent all expansion of variables as well.
Anyone familiar with classic UNIX shells will probably have seen these. POSIX supports the . command for executing shell commands from a file. The bash shell, and derivatives offer a synonym: source.
Create a spam1.sh with the following contents:
HELLO="Wide world\!"
Use . to load this, after setting to export mode.
unset HELLO
set -a # Set to export all shell variables to the environmentsource spam1.sh
set +a
python envtest.py
You can also use the . form, but then the specific path to the shell file (in this case ./) is required:
unset HELLO
set -a # Set to export all shell variables to the environment. ./spam1.sh
set +a
python envtest.py
Setting directly for subshell
Just as you want to minimize your use of global variables in programming, you probably want to minimize your use of global environment variables. You can set individual variables just for a given command:
This can of course get tedious, error-prone and hard to read, so you'll want to find ways to inject multiple environment variables as once.
Use python-dotenv [say what?!]
Yeah I started by saying don't use it, but actually it's fine to use it in command-line mode, to set up the environment externally to your program.
Sample env file code, spam1.env:
HELLO=Wide world!
Note that this env file format, AKA DotEnv format, informally AKA property file format is different from shell file format. No single or double quotes, no spaces around the quals sign (which would be ignored in shell format, but would be invalid in the .env format), and no need to escape !, $, etc.
Here it is in action:
dotenv -f spam1.env run -- python envtest.py
Output: Wide world!
ohmyzsh/dotenv
ohmyzsh has a dotenv extension that checks for a .env file every time you go to a new directory. This is neat, but it can also get tedious getting asked all the time, or it can get tricky if you tell it to do so without asking and then sometimes forget why variables have been set.
Check your launcher or other such app
I've noticed, for example, that it doesn't seem to be broad knowledge that the uvicorn server for Python/ASGI supports a --env-file which can read a file and pass environment variables defined therein to the ASGI application.
You really should be using a secrets manager or vault for your API keys, secrets, etc. I use 1passwd. You can create an environment file with URI-based references to secrets (e.g. op://app-prod/db/password; you can copy secrets reference URLs from the UI). The op run command then scans environment variables for such references, replaces them in the environment (not in the file) with the corresponding values from the app, so that a specified command is run in a subprocess with the secrets loaded.
op run --env-file=secrets-obscured.env -- python some_db_code.py
Note: I've experienced situations where op run ignores non-1password reference variables in files when the same variable is already exported in the shell, I suspect this is a bug, but just to be sure, I've become used to using a belt plus suspenders approach:
dotenv -f secrets-obscured.env run -- op run --env-file=secrets-obscured.env -- python some_db_code.py
Associating properties files with the .env file extension in VS Code
I noticed VS Code by default understands that files named exactly .env are properties files, but is not as smart with filenames such as dev.env or prod.env. You can fix this by setting the files.associations preference, e.g.:
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I published an updated version of this article on the Hugging Face blog. Pls refer to that instead.
I think the first time I even heard of the likes of python-dotenv and python-decouple were in association with LLM code, usually as a way of establishing API keys and other secrets. This hasn't ever really sat well with me. I think the program should inertly read the info passed in through the environment. Over time I've run into enough strange bugs in diverse codebases from such mixing to convince me of that gut feel. I think one of the healthiest things the twelve-factor app methodology has brought along is this understanding of the utility of the classic UNIX environment, but let's do it right: config management sets the environment, and the code reads it. No more leaking across this boundary.
I do admit that properly setting the environment can be trickier than it seems at first glance. Differences across platforms, and even across program launcher and shell variants can make things tricky, but that's kind of the point—you tailor the environment set-up to the host environment, and all that matters is that the correct values get across to the program.
Common tools/approaches for setting the environment
Note: this guide is very Linux/BSD-biased, and all code snippets are tested on Mac OS X under the zsh shell.
Sample Python code,
envtest.py:If you run:
HELLO="Wide world" python envtest.pyyou'll get
NOT FOUNDbecause the environment variable set in the first command is not automatically passed into the subshell for the python process of the second command.Modify the shell's environment so that it sticks for future commands
You can
exporta variable, which will then be injected into the child shell for subsequent commands (you can later useunsetto remove any variable):Output:
Wide worldNote: you can always view an env var value using
echoorprintenvYou might be tempted to use
echo:But the latter will surprise you in several situations, so
printenvas an easier to understand choice.You'll want to be familiar with some of the implications of shell substitutions and escaping. For example, if you tried to use an exclamation point in the value:
You'd run into problems because
!demarcates shell history expansion. You can use a backslash escape:Same with
$, which is used to demarcate shell variables.You can also use single quotes to avoid interpolation, but note that will prevent all expansion of variables as well.
There are other such characters you'd want to be aware of. Learn more ("Escaping Characters in Bash").
Good old dot or
sourceAnyone familiar with classic UNIX shells will probably have seen these. POSIX supports the
.command for executing shell commands from a file. The bash shell, and derivatives offer a synonym:source.Create a
spam1.shwith the following contents:HELLO="Wide world\!"Use
.to load this, after setting to export mode.You can also use the
.form, but then the specific path to the shell file (in this case./) is required:Setting directly for subshell
Just as you want to minimize your use of global variables in programming, you probably want to minimize your use of global environment variables. You can set individual variables just for a given command:
HELLO="Wide world\!" python envtest.pyOutput:
Wide world!HELLO='Wide world!' python envtest.pyYou can set multiple variables inline, as well:
This can of course get tedious, error-prone and hard to read, so you'll want to find ways to inject multiple environment variables as once.
Use python-dotenv [say what?!]
Yeah I started by saying don't use it, but actually it's fine to use it in command-line mode, to set up the environment externally to your program.
Sample env file code,
spam1.env:HELLO=Wide world!Note that this env file format, AKA DotEnv format, informally AKA property file format is different from shell file format. No single or double quotes, no spaces around the quals sign (which would be ignored in shell format, but would be invalid in the .env format), and no need to escape
!,$, etc.Here it is in action:
Output:
Wide world!ohmyzsh/dotenv
ohmyzsh has a dotenv extension that checks for a
.envfile every time you go to a new directory. This is neat, but it can also get tedious getting asked all the time, or it can get tricky if you tell it to do so without asking and then sometimes forget why variables have been set.Check your launcher or other such app
I've noticed, for example, that it doesn't seem to be broad knowledge that the uvicorn server for Python/ASGI supports a
--env-filewhich can read a file and pass environment variables defined therein to the ASGI application.Notes for Docker users
You can set environments in a variety of subtly different ways by using
ENVdirectives in Dockerfiles,-eondocker run, or the various ways you can set environment for docker compose.See also: "Passing Environment Variables to Docker Containers"
Special notes on secrets
You really should be using a secrets manager or vault for your API keys, secrets, etc. I use 1passwd. You can create an environment file with URI-based references to secrets (e.g.
op://app-prod/db/password; you can copy secrets reference URLs from the UI). Theop runcommand then scans environment variables for such references, replaces them in the environment (not in the file) with the corresponding values from the app, so that a specified command is run in a subprocess with the secrets loaded.Note: I've experienced situations where
op runignores non-1password reference variables in files when the same variable is already exported in the shell, I suspect this is a bug, but just to be sure, I've become used to using a belt plus suspenders approach:See also: "Secrets as environment variables in docker-compose files"
Associating properties files with the .env file extension in VS Code
I noticed VS Code by default understands that files named exactly
.envare properties files, but is not as smart with filenames such asdev.envorprod.env. You can fix this by setting thefiles.associationspreference, e.g.:Or through the GUI:
Beta Was this translation helpful? Give feedback.
All reactions