- Python
Python設定ファイルの読み込み|ini・TOML・YAML・JSON比較
Pythonで設定ファイルを読み込むには、iniなら configparser、TOMLなら tomllib(3.11以降)、JSONなら json という標準ライブラリを使い、YAMLは PyYAML、.env は python-dotenv を追加して読みます。 形式によって「読み込んだ値の型」が大きく違い、ini と .env はすべて文字列、TOML・YAML・JSON は数値や真偽値、リストとして受け取れます。
この記事では、同じ設定内容を5つの形式で書き分け、読み込みコードと取得結果を型つきで並べます。そのうえで、各形式でつまずきやすい点を実際のエラー文とともに示し、どれを選ぶかの指針をまとめます。掲載した出力は Python 3.11.15、PyYAML 6.0.3、python-dotenv 1.2.2、tomli-w 1.2.0 で実際に動かした結果です。
設定ファイル形式の早見表
| 比べる点 | ini | TOML | YAML | JSON | .env |
|---|---|---|---|---|---|
| 読み込むライブラリ | configparser |
tomllib |
PyYAML(yaml) |
json |
python-dotenv |
| 標準ライブラリか | 標準 | 標準(3.11以降) | 外部(pip install pyyaml) |
標準 | 外部(pip install python-dotenv) |
| コメント | 書ける(; と #) |
書ける(#) |
書ける(#) |
書けない | 書ける(#) |
| 読み込んだ値の型 | すべて str | str / int / float / bool / 日付 / list / dict | str / int / float / bool / 日付 / list / dict / None | str / int / float / bool / list / dict / None | すべて str |
| ネスト(入れ子) | セクション1段のみ | できる(テーブル) | できる(インデント) | できる | できない |
| リスト | ない(自分で分割) | ある | ある | ある | ない(自分で分割) |
| 書き込み | 標準でできる | 読み込みのみ。書き込みは tomli-w | できる(safe_dump) |
できる(json.dump) |
できる(set_key) |
| 書き込み時にコメントが残るか | 残らない | 残らない(tomli-w) | 残らない | ― | 残る |
| 主な使いどころ | 単純なキーと値、昔からあるツール | Python のツール設定、アプリの設定 | ネストの深い設定、インフラ系ツール | プログラム同士の受け渡し | 環境ごとに変わる値、秘密の値 |
迷ったら、Python 3.11 以降で新しく作る設定は TOML が扱いやすい選択です。 標準ライブラリで読めて、コメントが書けて、型がそのまま受け取れます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python設定ファイルの読み込み|ini・TOML・YAML・JSON比較」
同じ設定を5つの形式で書き分ける
題材は、アプリ名・デバッグの有無・ポート番号・タイムアウト秒数・許可するホストの一覧と、データベースの接続先です。
ini(config.ini)
; アプリの設定(iniのコメントは ; か #)
[app]
name = inventory
debug = true
port = 8080
timeout = 2.5
allowed_hosts = localhost, example.com
[database]
host = db.example.com
port = 5432
TOML(config.toml)
# アプリの設定
[app]
name = "inventory"
debug = true
port = 8080
timeout = 2.5
allowed_hosts = ["localhost", "example.com"]
started = 2026-10-03
[database]
host = "db.example.com"
port = 5432
YAML(config.yaml)
# アプリの設定
app:
name: inventory
debug: true
port: 8080
timeout: 2.5
allowed_hosts:
- localhost
- example.com
started: 2026-10-03
database:
host: db.example.com
port: 5432
JSON(config.json)
{
"app": {
"name": "inventory",
"debug": true,
"port": 8080,
"timeout": 2.5,
"allowed_hosts": ["localhost", "example.com"]
},
"database": {
"host": "db.example.com",
"port": 5432
}
}
.env
# アプリの設定
APP_NAME=inventory
APP_DEBUG=true
APP_PORT=8080
APP_TIMEOUT=2.5
APP_ALLOWED_HOSTS=localhost,example.com
DATABASE_HOST=db.example.com
DATABASE_PORT=5432
見た目の違いをまとめると次のとおりです。
| 形式 | 文字列の引用符 | 階層の表し方 | リストの書き方 |
|---|---|---|---|
| ini | 付けない | [セクション] |
カンマ区切りの文字列にして自分で分割 |
| TOML | 必ず付ける | [テーブル] |
["a", "b"] |
| YAML | 付けなくてよい | インデント(スペース) | - a を並べる |
| JSON | 必ず二重引用符 | { } の入れ子 |
["a", "b"] |
| .env | 付けなくてよい | ない(名前に接頭辞を付ける) | カンマ区切りの文字列にして自分で分割 |
5形式を読み込んで、値と型を横並びで比べる
# read_all.py
# 事前に: pip install pyyaml python-dotenv
import configparser
import json
import tomllib
import yaml
from dotenv import dotenv_values
def show(label, value):
print(f"{label:5} {value!r:28} {type(value).__name__}")
ini = configparser.ConfigParser()
ini.read("config.ini", encoding="utf-8")
with open("config.toml", "rb") as f:
toml = tomllib.load(f)
with open("config.yaml", encoding="utf-8") as f:
yml = yaml.safe_load(f)
with open("config.json", encoding="utf-8") as f:
js = json.load(f)
env = dotenv_values(".env")
for key in ("name", "debug", "port", "timeout", "allowed_hosts"):
print(f"--- {key}")
show("ini", ini["app"][key])
show("toml", toml["app"][key])
show("yaml", yml["app"][key])
show("json", js["app"][key])
show(".env", env["APP_" + key.upper()])
print("--- started")
show("toml", toml["app"]["started"])
show("yaml", yml["app"]["started"])
python read_all.py
--- name
ini 'inventory' str
toml 'inventory' str
yaml 'inventory' str
json 'inventory' str
.env 'inventory' str
--- debug
ini 'true' str
toml True bool
yaml True bool
json True bool
.env 'true' str
--- port
ini '8080' str
toml 8080 int
yaml 8080 int
json 8080 int
.env '8080' str
--- timeout
ini '2.5' str
toml 2.5 float
yaml 2.5 float
json 2.5 float
.env '2.5' str
--- allowed_hosts
ini 'localhost, example.com' str
toml ['localhost', 'example.com'] list
yaml ['localhost', 'example.com'] list
json ['localhost', 'example.com'] list
.env 'localhost,example.com' str
--- started
toml datetime.date(2026, 10, 3) date
yaml datetime.date(2026, 10, 3) date
結果を表にまとめます。
| 設定 | ini | TOML | YAML | JSON | .env |
|---|---|---|---|---|---|
| name | str | str | str | str | str |
| debug | str('true') | bool | bool | bool | str('true') |
| port | str('8080') | int | int | int | str('8080') |
| timeout | str('2.5') | float | float | float | str('2.5') |
| allowed_hosts | str | list | list | list | str |
| started(日付) | ― | date | date | 日付型がない | ― |
ini と .env は、すべての値が文字列で返ります。 数値や真偽値として使うには変換が必要です。TOML・YAML・JSON はファイルに書いた型のまま受け取れます。日付を型として持てるのは TOML と YAML です。
読み込みコードの要点を1行ずつにすると、次のようになります。
| 形式 | 読み込みコード | ファイルを開くモード |
|---|---|---|
| ini | c = configparser.ConfigParser(); c.read(path, encoding="utf-8") |
パスを渡す(自分で開かない) |
| TOML | with open(path, "rb") as f: data = tomllib.load(f) |
バイナリ("rb") |
| YAML | with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) |
テキスト |
| JSON | with open(path, encoding="utf-8") as f: data = json.load(f) |
テキスト |
| .env | values = dotenv_values(path) または load_dotenv(path) |
パスを渡す |
configparserでiniを読むときの3つのポイント
値が全部文字列になる → getint・getboolean で変換する
上の結果のとおり、ini["app"]["port"] は '8080' という文字列です。特に真偽値は気づきにくく、次のようになります。
import configparser
ini = configparser.ConfigParser()
ini.read("config.ini", encoding="utf-8")
print(bool(ini["app"]["debug"])) # 'true' という文字列
print(bool("false")) # 'false' でも結果は同じ
True
True
空でない文字列は、中身が "false" でも真と評価されます。 型つきで受け取るメソッドを使うと解決します。
app = ini["app"]
print(app.getboolean("debug"), app.getint("port"), app.getfloat("timeout"))
print([h.strip() for h in app["allowed_hosts"].split(",")])
print(app.getint("retries", fallback=3)) # キーがないときの既定値
True 8080 2.5
['localhost', 'example.com']
3
| メソッド | 返す型 | 受け付ける値 |
|---|---|---|
get(key) / section[key] |
str | そのまま |
getint(key) |
int | 整数として読める文字列 |
getfloat(key) |
float | 小数として読める文字列 |
getboolean(key) |
bool | 1 / yes / true / on と 0 / no / false / off(大文字小文字は区別しない) |
get(key, fallback=値) |
str または fallback | キーがないとき fallback を返す |
変換できない値が入っていると、その場でエラーになります。
ValueError: Not a boolean: maybe
ValueError: invalid literal for int() with base 10: '80a'
キーやセクションがないときのエラーは、取り出し方で変わります。
| 書き方 | キーがないとき |
|---|---|
ini["app"]["missing"] |
KeyError: 'missing' |
ini.get("app", "missing") |
NoOptionError: No option 'missing' in section: 'app' |
ini.get("nosec", "x") |
NoSectionError: No section: 'nosec' |
ini.get("app", "missing", fallback="none") |
'none' を返す |
もう1つ知っておくと役立つのが、存在しないファイルを read() してもエラーにならないことです。
c = configparser.ConfigParser()
print(c.read("nothing.ini"), c.sections())
[] []
read() は読み込めたファイル名のリストを返すので、空リストかどうかを確かめると、パスの間違いに早く気づけます。
%を含む値で補間エラーになる → %% と書くか interpolation=None
configparser には、%(名前)s と書くと同じセクションの別の値を埋め込む補間という機能があります。そのため、日付の書式のように % を含む値を書くとエラーになります。
; pct.ini
[log]
format = %Y-%m-%d %H:%M
rate = 50%
import configparser
c = configparser.ConfigParser()
c.read("pct.ini")
print(c["log"]["format"])
InterpolationSyntaxError: '%' must be followed by '%' or '(', found: '%Y-%m-%d %H:%M'
rate = 50% を取り出したときも同じ種類のエラーです。
InterpolationSyntaxError: '%' must be followed by '%' or '(', found: '%'
エラーは読み込み時ではなく、値を取り出した時に出ます。 直し方は2つあります。
# 直し方1:補間を使わないなら、機能を止める
c = configparser.ConfigParser(interpolation=None)
c.read("pct.ini")
print(c["log"]["format"], c["log"]["rate"])
%Y-%m-%d %H:%M 50%
# 直し方2:補間を使いたいなら、ファイル側で % を %% と書く
c = configparser.ConfigParser()
c.read_string("""
[log]
format = %%Y-%%m-%%d
[paths]
base = /var/app
logs = %(base)s/logs
""")
print(c["log"]["format"], c["paths"]["logs"])
%Y-%m-%d /var/app/logs
| 状況 | 選ぶ方法 |
|---|---|
| 補間を使っていない | ConfigParser(interpolation=None) |
%(base)s のような埋め込みを使っている |
文字としての % を %% と書く |
| そもそも記号を多く含む値を置きたい | TOML など補間のない形式に移す |
キーが小文字になる → optionxform = str
import configparser
c = configparser.ConfigParser()
c.read_string("""
[App]
UserName = taro
API_KEY = abc
""")
print(c.sections(), list(c["App"].keys()))
print(c["App"]["username"], c["App"]["USERNAME"])
['App'] ['username', 'api_key']
taro taro
キー(オプション名)は小文字に変換されて保存されます。 取り出すときも小文字にしてから探すので、USERNAME でも取れます。一方、セクション名は大文字小文字を区別するので、c["app"] は KeyError: 'app' になります。
キーの大文字小文字を保ちたいときは、読み込む前に optionxform を差し替えます。
c = configparser.ConfigParser()
c.optionxform = str
c.read_string("[App]\nUserName = taro\nAPI_KEY = abc\n")
print(list(c["App"].keys()))
['UserName', 'API_KEY']
そのほかの仕様と、iniへの書き込み
| 仕様 | 実行結果 |
|---|---|
[DEFAULT] に書いた値は全セクションから見える |
[DEFAULT] timeout = 10 があると、dict(c["app"]) は {'name': 'a', 'timeout': '10'} |
| 同じセクションに同じキーを2回書く | DuplicateOptionError: While reading from '<string>' [line 3]: option 'name' in section 'app' already exists |
| 文字列以外を代入する | c["app"]["retries"] = 3 は TypeError: option values must be strings |
書き込みは標準でできます。値は文字列にして代入します。
import configparser
c = configparser.ConfigParser()
c.read("config.ini", encoding="utf-8")
c["app"]["port"] = "9090"
c["app"]["retries"] = str(3)
c["cache"] = {"enabled": "false"}
with open("out.ini", "w", encoding="utf-8") as f:
c.write(f)
[app]
name = inventory
debug = true
port = 9090
timeout = 2.5
allowed_hosts = localhost, example.com
retries = 3
[database]
host = db.example.com
port = 5432
[cache]
enabled = false
元のファイルの1行目にあったコメントは、書き出したファイルには残りませんでした。人がコメントを書き込むファイルをプログラムで上書きするときは、この点を踏まえて設計します。
tomllibでTOMLを読むときの3つのポイント
"rb" で開かないと TypeError になる
import tomllib
with open("config.toml") as f: # テキストモード
data = tomllib.load(f)
TypeError: File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`
encoding="utf-8" を付けても同じです。TOML は仕様で UTF-8 と決まっているので、tomllib がバイト列から自分で解釈します。open(path, "rb") に直すと読めます。
| 関数 | 渡すもの |
|---|---|
tomllib.load(f) |
"rb" で開いたファイル |
tomllib.loads(s) |
文字列(str) |
文字列を持っている場合は loads を使います。
with open("config.toml", encoding="utf-8") as f:
data = tomllib.loads(f.read())
print(data["database"])
{'host': 'db.example.com', 'port': 5432}
書き込みは tomli-w を追加する
tomllib は読み込み専用です。公開されている名前を確かめると、3つだけでした。
import tomllib
print([n for n in dir(tomllib) if not n.startswith("_")])
tomllib.dump
['TOMLDecodeError', 'load', 'loads']
AttributeError: module 'tomllib' has no attribute 'dump'
書き出すには、外部ライブラリの tomli-w を使います。
# 事前に: pip install tomli-w
import tomllib
import tomli_w
with open("config.toml", "rb") as f:
data = tomllib.load(f)
data["app"]["port"] = 9090
data["cache"] = {"enabled": False, "ttl": 300}
with open("out.toml", "wb") as f: # 書き込みもバイナリ
tomli_w.dump(data, f)
[app]
name = "inventory"
debug = true
port = 9090
timeout = 2.5
allowed_hosts = [
"localhost",
"example.com",
]
started = 2026-10-03
[database]
host = "db.example.com"
port = 5432
[cache]
enabled = false
ttl = 300
| つまずく点 | 実際のメッセージ | 直し方 |
|---|---|---|
"w" で開いて tomli_w.dump |
TypeError: write() argument must be str, not bytes |
"wb" で開く。または tomli_w.dumps(data) で文字列を得る |
| 値に None が入っている | TypeError: Object of type 'NoneType' is not TOML serializable |
TOML には null がないので、キーごと省く |
| コメントが消える | 元の1行目のコメントは出力に残らなかった | コメントを保ちたい場合は tomlkit などの専用ライブラリを検討(この記事では未実行) |
TOMLの書き方でエラーになる例
TOML は型をはっきり書く形式なので、書き方が違うとその場で教えてくれます。
| 書いた内容 | 結果 | 直し方 |
|---|---|---|
name = inventory |
TOMLDecodeError: Invalid value (at line 1, column 8) |
文字列は "inventory" と引用符で囲む |
debug = True |
TOMLDecodeError: Invalid value (at line 1, column 9) |
真偽値は小文字で true |
port = 08 |
TOMLDecodeError: Expected newline or end of document after a statement (at line 1, column 9) |
先頭の 0 は付けない。8 と書く |
n = null |
TOMLDecodeError: Invalid value (at line 1, column 5) |
null はない。キーを書かない |
| 同じキーを2回書く | TOMLDecodeError: Cannot overwrite a value (at end of document) |
片方を消す |
エラーに行と列が出るので、直す場所がすぐ分かります。ネストや配列の書き方も確かめました。
[[servers]]
name = "a"
[[servers]]
name = "b"
[app.log]
level = "INFO"
inline = { x = 1, y = 2 }
{'servers': [{'name': 'a'}, {'name': 'b'}], 'app': {'log': {'level': 'INFO', 'inline': {'x': 1, 'y': 2}}}}
[[名前]] は「辞書のリスト」、[a.b] は入れ子の辞書になります。Windows のパスのように \ を含む値は、'C:\Users\taro' と一重引用符で囲むとそのまま読めました。
Python 3.10 以前では tomllib がないので、同じ使い方ができる外部ライブラリ tomli を入れて import tomli as tomllib と書く方法が広く使われています(この記事では未実行)。
YAMLは yaml.load ではなく safe_load を使う
safe_load を使う理由
YAML には、タグという仕組みで「この値は Python のこのオブジェクトとして作る」と指示する書き方があります。PyYAML の制限のないローダーは、この指示に従って Python の関数を呼び出します。害のない os.getcwd(今いるフォルダを返す関数)で確かめました。
import os
import yaml
doc = "!!python/object/apply:os.getcwd []"
# 制限のないローダー:YAML に書かれた関数が実行される
print(yaml.load(doc, Loader=yaml.UnsafeLoader) == os.getcwd())
# safe_load:読み込みを止めてエラーにする
yaml.safe_load(doc)
True
ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply:os.getcwd'
in "<unicode string>", line 1, column 1:
!!python/object/apply:os.getcwd []
^
1つ目の True は、YAML を読み込んだだけで関数が実行されたことを示しています。設定ファイルは、人から受け取ったり、ダウンロードしたりすることもあります。safe_load は文字列・数値・リスト・辞書といった基本の型だけを作るので、こうした指示が入っていても実行せずにエラーで知らせてくれます。設定ファイルの読み込みには yaml.safe_load を使う、と決めておくと安心です。
Loader=yaml.FullLoader でも、この例は同じ ConstructorError になりました。なお、PyYAML 6 では Loader を省いた yaml.load(f) はそのままでは動きません。
yaml.load(open("config.yaml", encoding="utf-8"))
TypeError: load() missing 1 required positional argument: 'Loader'
| 書き方 | 動き | 使いどころ |
|---|---|---|
yaml.safe_load(f) |
基本の型だけを作る | 設定ファイルの読み込み(これを使う) |
yaml.load(f, Loader=yaml.SafeLoader) |
safe_load と同じ | 同上 |
yaml.load(f) |
PyYAML 6 では TypeError | ― |
yaml.load(f, Loader=yaml.UnsafeLoader) |
YAML 内の指示で Python の関数を呼べる | 設定ファイルには使わない |
YAMLで型が思ったとおりにならない例
YAML は引用符を省けるぶん、値の型を自動で決めます。次の内容を safe_load で読んだ結果です。
country: NO
enabled: yes
switch: on
version: 1.10
zip: 0123
time: 12:30
empty:
quoted: 'NO'
ver2: '1.10'
{'country': False, 'enabled': True, 'switch': True, 'version': 1.1, 'zip': 83, 'time': 750, 'empty': None, 'quoted': 'NO', 'ver2': '1.10'}
| 書いた値 | 読み込んだ結果 | 理由 | 文字列にしたいとき |
|---|---|---|---|
NO |
False(bool) |
yes / no / on / off は真偽値として扱われる | 'NO' と引用符で囲む |
1.10 |
1.1(float) |
小数として解釈される | '1.10' |
0123 |
83(int) |
先頭が 0 の数字は8進数として解釈される | '0123' |
12:30 |
750(int) |
60進数として解釈される | '12:30' |
| (空) | None |
値がないと null になる | '' |
バージョン番号・コード・時刻のように「文字列として扱いたい値」は、引用符で囲むと確実です。
インデントの乱れもよくあるつまずきです。
| 書き方 | エラー |
|---|---|
| インデントにタブを使う | ScannerError: while scanning for the next token / found character '\t' that cannot start any token |
| インデントの幅がそろっていない | ScannerError: mapping values are not allowed here |
同じキーを2回書いた場合はエラーにならず、後に書いた値が使われました(a: 1 と a: 2 で {'a': 2})。
YAMLへの書き込み
import yaml
with open("config.yaml", encoding="utf-8") as f:
data = yaml.safe_load(f)
data["app"]["port"] = 9090
print(yaml.safe_dump(data, allow_unicode=True, sort_keys=False))
app:
name: inventory
debug: true
port: 9090
timeout: 2.5
allowed_hosts:
- localhost
- example.com
started: 2026-10-03
database:
host: db.example.com
port: 5432
| 引数 | 効果 |
|---|---|
allow_unicode=True |
日本語をそのまま出力する。付けないとエスケープ表記になる({"名前": "在庫"} で確認) |
sort_keys=False |
キーを元の順番のまま出力する。既定ではアルファベット順に並べ替える |
コメントは出力に残りません。
JSONを設定ファイルに使うときのポイント
JSON は標準ライブラリで読み書きでき、ほぼすべての言語が対応しています。設定ファイルとして使うときは、コメントが書けないことと書式が厳密なことを押さえておきます。
| 書いた内容 | エラー | 直し方 |
|---|---|---|
{"a": 1,}(末尾のカンマ) |
JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 9 (char 8) |
最後のカンマを消す |
{'a': 1}(一重引用符) |
JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1) |
二重引用符にする |
{"a": 1} // memo(コメント) |
JSONDecodeError: Extra data: line 1 column 10 (char 9) |
コメントを消す。説明は別のキーやドキュメントに書く |
{"a": True}(Python の書き方) |
JSONDecodeError: Expecting value: line 1 column 7 (char 6) |
true / false / null と小文字で書く |
同じキーを2回書くと、YAML と同じく後の値が使われました({"a": 1, "a": 2} は {'a': 2})。
書き込みは json.dump です。
import json
with open("config.json", encoding="utf-8") as f:
data = json.load(f)
data["app"]["port"] = 9090
with open("out.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
| 引数 | 効果 |
|---|---|
ensure_ascii=False |
日本語をそのまま出力する({"名前": "在庫"})。付けないとエスケープ表記になる |
indent=2 |
改行とインデントを入れて読みやすくする |
日付型はそのまま書き出せません。
TypeError: Object of type date is not JSON serializable
日付は "2026-10-03" のような文字列で持ち、読み込んだ後に date.fromisoformat() で変換します。
.envは環境ごとに変わる値を置く場所
.env は、接続先やAPIキーのように環境ごとに変わる値や、リポジトリに入れたくない値を置くための形式です。python-dotenv で読み込みます。
# 事前に: pip install python-dotenv
import os
from dotenv import dotenv_values, load_dotenv
# 1. 辞書として受け取る(環境変数は変えない)
values = dotenv_values(".env")
print(values["APP_PORT"], type(values["APP_PORT"]).__name__)
# 2. 環境変数に読み込む
print(os.environ.get("APP_PORT"))
print(load_dotenv(".env"))
print(os.environ.get("APP_PORT"))
# 3. 型を変換して使う
port = int(os.environ["APP_PORT"])
debug = os.environ["APP_DEBUG"].lower() in ("1", "true", "yes", "on")
hosts = os.environ["APP_ALLOWED_HOSTS"].split(",")
print(port, debug, hosts)
8080 str
None
True
8080
8080 True ['localhost', 'example.com']
| 知っておくこと | 実行結果 |
|---|---|
| 値はすべて文字列 | '8080'(str)。int や bool は自分で変換する |
| 既にある環境変数は上書きしない | OS 側で APP_NAME=from_os を設定してから load_dotenv() すると from_os のまま |
| 上書きしたいとき | load_dotenv(".env", override=True) で .env の値(inventory)になった |
ないキーを os.environ["APP_MISSING"] で読む |
KeyError: 'APP_MISSING' |
| 既定値つきで読む | os.environ.get("APP_MISSING", "default") は 'default' |
set_key("out.env", "APP_PORT", "9090") で書き換える |
その行が APP_PORT='9090' になり、1行目のコメントは残った |
「OS の環境変数が優先、.env は足りない分を補う」という動きが既定なので、本番環境では環境変数、手元では .env、という使い分けがしやすくなっています。.env ファイルは .gitignore に入れ、キー名だけを書いた .env.example を共有する方法がよく使われます。
どの形式を選ぶかの指針
| こんな設定 | 選ぶ形式 | 理由 |
|---|---|---|
| Python 3.11 以降で新しく作るアプリの設定 | TOML | 標準で読める、コメント可、型がそのまま |
| pyproject.toml に寄せたいツール設定 | TOML | Python のツールが共通で使う場所 |
| ネストが深い、リストの中に辞書が並ぶ | YAML | インデントで構造が見やすい |
| ほかの言語やツールが既に YAML を読んでいる | YAML | 既存の仕組みに合わせる |
| プログラムが書き出し、プログラムが読む | JSON | どの言語でも標準で読み書きできる |
| 人が手で編集し、説明を添えたい | TOML か YAML | コメントが書ける |
| 接続先やAPIキーなど環境ごとに変わる値 | .env(環境変数) | リポジトリに入れずに切り替えられる |
| キーと値だけの単純な設定、既存の ini がある | ini | 標準で読み書きできる。型変換は getint などで |
| 外部ライブラリを入れられない | ini / JSON / TOML(3.11以降は読み込みのみ) | すべて標準ライブラリ |
flowchart TD
A[設定ファイルを決める] --> B{環境ごとに変わる値や秘密の値か}
B -- はい --> C[.env と環境変数]
B -- いいえ --> D{人が手で編集するか}
D -- いいえ --> E[JSON]
D -- はい --> F{ネストが深いか 既にYAMLを使っているか}
F -- はい --> G[YAML と safe_load]
F -- いいえ --> H{Python 3.11以降か}
H -- はい --> I[TOML と tomllib]
H -- いいえ --> J[ini と configparser または tomli を追加]
1つに絞る必要はなく、「アプリの設定は TOML、秘密の値は .env」のように組み合わせるのが一般的です。
読み込んだ設定を型つきのクラスで受ける
辞書のまま使うと、キーの打ち間違いに気づくのが実行時になります。dataclass に詰め替えると、エディタの補完が効き、型も明確になります。
import tomllib
from dataclasses import dataclass
@dataclass(frozen=True)
class AppConfig:
name: str
debug: bool
port: int
timeout: float
allowed_hosts: list[str]
def load_config(path: str) -> AppConfig:
with open(path, "rb") as f:
data = tomllib.load(f)
app = data["app"]
return AppConfig(
name=app["name"],
debug=app["debug"],
port=app["port"],
timeout=app["timeout"],
allowed_hosts=app["allowed_hosts"],
)
config = load_config("config.toml")
print(config)
print(config.port + 1)
AppConfig(name='inventory', debug=True, port=8080, timeout=2.5, allowed_hosts=['localhost', 'example.com'])
8081
frozen=True を付けると、読み込んだ後に値を書き換えられなくなり、設定が途中で変わる心配がなくなります。必須のキーがファイルにないときは、app["name"] の行で KeyError になるので、起動時に気づけます。
つまずきやすい点と直し方(形式別まとめ)
| 形式 | 起きること | 直し方 |
|---|---|---|
| ini | if ini["app"]["debug"]: がいつも真 |
getboolean("debug") を使う |
| ini | InterpolationSyntaxError(% を含む値) |
interpolation=None か、%% と書く |
| ini | キーが小文字になる | optionxform = str |
| ini | 設定が空のまま動く | read() の戻り値が空リストでないか確かめる |
| TOML | TypeError: File must be opened in binary mode |
open(path, "rb") |
| TOML | module 'tomllib' has no attribute 'dump' |
tomli-w を使う |
| TOML | Invalid value |
文字列を引用符で囲む。真偽値は小文字 |
| YAML | NO や 1.10 が別の型になる |
引用符で囲む |
| YAML | load() missing 1 required positional argument: 'Loader' |
yaml.safe_load に変える |
| YAML | タブでインデントして ScannerError | スペースでそろえる |
| JSON | 末尾のカンマ、コメントで JSONDecodeError | 取り除く |
| .env | 数値のつもりが文字列 | int() などで変換する |
| .env | .env を変えたのに値が変わらない | 既に環境変数がある。override=True か環境変数を見直す |
よくある質問
Q. Python 3.10 以前で TOML を読むにはどうすればよいですか?
A. tomllib は 3.11 で標準に入りました。3.10 以前では外部ライブラリの tomli を入れ、import tomli as tomllib と書くと同じ使い方ができることが知られています(この記事では未実行です)。
Q. 設定を Python ファイル(config.py)に書くのはどうですか? A. import するだけで読めて型もそのまま使えるので、小さなスクリプトでは手軽です。一方、設定ファイルがプログラムとして実行されることになるため、利用者が編集するファイルや外から受け取るファイルには、データだけを書ける TOML や YAML が向いています。
Q. コメントを保ったまま設定ファイルを書き換えたいときは?
A. この記事で確かめた範囲では、configparser・tomli-w・PyYAML の書き出しではコメントが残らず、python-dotenv の set_key では残りました。TOML なら tomlkit、YAML なら ruamel.yaml といった、コメントを保つことを目的にしたライブラリがあります(この記事では未実行です)。
Q. 複数の設定ファイルを重ねて読みたいときは?
A. configparser の read() はファイル名のリストを受け取り、後のファイルの値で上書きします。TOML や YAML では、既定値の辞書を読み込んでから、環境別のファイルの辞書で上書きする形にします。「既定値 → ファイル → 環境変数」の順に優先度を上げていくと、切り替えが分かりやすくなります。
まとめ
- ini と .env は値がすべて文字列。TOML・YAML・JSON は数値・真偽値・リストをそのまま受け取れる
- configparser は getint / getboolean で型変換。% を含む値は
interpolation=Noneか%%、キーの小文字化はoptionxform = str - tomllib は "rb" で開く。読み込み専用なので、書き込みは tomli-w を追加する
- YAML は
yaml.safe_load。NO や 1.10 のような値は引用符で囲むと文字列のまま読める - JSON はコメントと末尾カンマが書けない。プログラム同士の受け渡しに向く
- 環境ごとに変わる値や秘密の値は .env と環境変数へ。既にある環境変数が優先される
- 迷ったら TOML。ネストが深ければ YAML、機械が読み書きするなら JSON
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python設定ファイルの読み込み|ini・TOML・YAML・JSON比較」
株式会社RJCでは、Pythonの設定ファイルの扱い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。