環境変数で設定を管理したくなった場合のpython-dotenvのメモ

最近は設定ファイルだけではなく環境変数で設定を渡すことがけっこうある(コンテナ的な文脈だったり、12 factor app的な文脈だったり色々)。自前で頑張っても良いけれど。自分でコードを書かずに済ませたい。

何か良いライブラリは無いかなと探してみたらあったので使いかたのメモ。

github.com

python-dotenv

Get and set values in your .env file in local and production servers. tada

install

$ pip install python-dotenv

挙動

getting startedを読むだけでは挙動を把握できなかったので色々調べてみた。

挙動を一言で言うと、os.environに対してdefault値を与えるもの。

たとえば以下のような.envがあるとする。

.env

prefix=hello
message=${prefix} world

このとき環境変数を指定して実行せずともdefault値として.envに記述された値が使われる。 (正確に言うなら環境変数として指定された名前の値を保持していない場合に.envから読み込む)

main.py

import os
from dotenv import load_dotenv

load_dotenv(verbose=True)
print("prefix", os.environ.get("prefix"))
print("message", os.environ.get("message"))

ここで

defaultは.envに記述された値

$ python main.py
prefix hello
message hello world

環境変数を与えるとその値が使われる

$ prefix=bye python main.py
prefix bye
message bye world

case sensitive(大文字小文字を区別する)

$ PREFIX=bye python main.py
prefix hello
message hello world

なのでlocal用のaccess tokenを.envに記述し本番では環境変数越しに値を渡すということが想定された使われ方(12 factor app的な文脈)。

override=True

override=Trueを使うとdotenv側の設定を尊重する。正直使いみちはあまりないと思う。

$ python main2.py
prefix hello
message hello world

$ prefix=bye python main2.py
prefix hello
message hello world

環境変数で指定した値が使われなくなる。

変更のdiff

--- main.py  2019-04-29 15:48:35.499386628 +0900
+++ main2.py  2019-04-29 16:29:58.014889108 +0900
@@ -1,6 +1,6 @@
 import os
 from dotenv import load_dotenv
 
-load_dotenv(verbose=True)
+load_dotenv(verbose=True, override=True)
 print("prefix", os.environ.get("prefix"))
 print("message", os.environ.get("message"))

.envで設定した部分だけを取り出す

これは dotenv_values() で取り出せる。configとして扱う分にはこちらのほうが便利かもしれない。

from dotenv import load_dotenv, dotenv_values

load_dotenv(verbose=True)
print(dotenv_values(verbose=True))

実行結果

{'prefix': 'hello', 'message': 'hello world'}

このときの.env

prefix=hello
message=${prefix} world

追記

:warning: 環境変数込みで渡した場合にも.env側の値が取り出されるので、configとして使うには危険かも(実装を考えるとこの挙動はあり得る)

$ python main.py
{'prefix': 'hello', 'message': 'hello world'}

$ prefix=bye python main*.py
{'prefix': 'hello', 'message': 'bye world'}

prefixは.envに書かれたものが使われる。一方messageは環境変数の解釈が入りhelloではなくbyeが使われる。

ちょっとひと手間だけれど、以下の様にすれば期待したふるまいになりそう。

import os

print({k: os.environ.get(k) for k in dotenv_values()})
# {'prefix': 'bye', 'message': 'bye world'}

複数の.envが用意されていた場合に読まれる.envについて

これはfind_dotenv()の実行で確認できる。複数のファイルを全部マージするなどと言ったマジカルな挙動は存在しない。最初に見つかった.envファイルを使う。それでおしまい。

ただし現在の位置に存在しなかった場合に親方向を辿って探してはくれる。

$ tree -a
.
|-- .env
|-- main.py
|-- sub
|   |-- .env
|   `-- main.py
`-- sub2
    `-- main.py

2 directories, 5 files

ここでそれぞれ見られる.envは以下の様な形。

  • main.py は.env
  • sub/main.py は sub/.env
  • sub2/main.py は .env

調査用のコードは以下のようなもの。

main.py

from dotenv import find_dotenv, dotenv_values

print(find_dotenv())
print(dotenv_values())

実行結果

$ python main.py
~/my/example_dotenv/03find/.env
{'x': 'root'}
$ python sub/main.py
~/my/example_dotenv/03find/sub/.env
{'x': 'sub', 'y': 'value'}

$ python sub2/main.py
~/my/example_dotenv/03find/.env
{'x': 'root'}

このときの.env

.env

x="root"

sub/.env

x=sub
y=value

subprocessの実行時の環境変数

subprocessに環境変数を引き継ぐ方法は通常のsubprocessの利用と変わらない。dotenvで読み込んだときに環境変数に設定するので。むしろこれのために環境変数ベースで設定をしていると言っても良い(processとsubprocessとの間でのdynamic scope的な辞書)。

たとえば先程の.env(再掲)を以下の様なsubprocessを利用したコードで使う。

.env

prefix=hello
message=${prefix} world

subprocessを利用したコード

main2.py

import subprocess
from dotenv import load_dotenv

load_dotenv(verbose=True)


cmd = ["python", "-c", "import os; print(os.environ.get('message', '<none>'))"]
subprocess.run(cmd, check=True)

messageという環境変数が引き継がれる。

$ python main2.py
hello world

個別に設定したい場合

これはオフトピックだけれど、個別に設定したい場合はsubprocess.runenvオプションに辞書を渡せば良い。

main3.py

import os
import subprocess
from dotenv import load_dotenv

load_dotenv(verbose=True)


cmd = ["python", "-c", "import os; print(os.environ.get('message', '<none>'))"]

myenv = os.environ.copy()
myenv["message"] = "bye, bye"

subprocess.run(cmd, check=True)
subprocess.run(cmd, check=True, env=myenv)
subprocess.run(cmd, check=True)

結果

$ python main3.py
hello world
bye, bye
hello world