Error [configparser.InvalidWriteError: Cannot write key a=b; contains delimiter =]

configparser.InvalidWriteError: Cannot write key ... contains delimiter の直し方(Python 3.14)

FIX SUMMARY verified
Applies when
python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1Writing a ConfigParser key that contains a default delimiter (= or :); Python 3.14 raises InvalidWriteError instead of emitting a file that reads back as a different key

Verified: reproduced in python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1, then the configparser.InvalidWriteError: Cannot write key a=b; contains delimiter = signature was gone after the fix (exit 0).

Python を 3.14 に上げたら、設定ファイルの書き出しが落ちます。

configparser.InvalidWriteError: Cannot write key a=b; contains delimiter =

コピペ検索用の表記ゆれです(どちらも実測)。

  • configparser.InvalidWriteError: Cannot write key a=b; contains delimiter =
  • configparser.InvalidWriteError: Cannot write key a:b; contains delimiter :

InvalidWriteError は Python 3.14 で新設された例外です。 3.13 以前には存在しません。

3.13 では、このコードは何をしていたのか

同じコードは 3.13 まで終了コード 0 で通ります。警告も出ません。

import configparser, io

config = configparser.ConfigParser()
config["settings"] = {"a=b": "value"}   # キーに = が入っている

buffer = io.StringIO()
config.write(buffer)
print(buffer.getvalue())

3.13 が書き出す中身はこれです。

[settings]
a=b = value

これを読み戻すと、キーが変わります。

back = configparser.ConfigParser()
back.read_string("[settings]\na=b = value\n")
print(dict(back["settings"]))
# 3.12 / 3.13 / 3.14 いずれも → {'a': 'b = value'}

a=b というキーは消えています。 INI の書式では最初に現れた区切り文字で行を割るので、a=b = value は「キー a、値 b = value」として読まれます。

3.14 のエラーは、この書き込みを止めています。 3.13 まで、このコードは読み戻せないファイルを黙って生成していました

直し方

キーから区切り文字を外します。 = は値の側に置けます。

config["settings"] = {"a_b": "b=c"}
[settings]
a_b = b=c

これは既定の ConfigParser でそのまま読み戻せます({'a_b': 'b=c'}・実測)。検査はキーだけを見るので、値に = が入っているのは問題になりません。

キーが外部から来ていて改名できない場合は、キーを符号化して(urllib.parse.quote などで)区切り文字を消すか、そもそも INI 以外の形式に移します。JSON や TOML には「キーに区切り文字を書けない」という制約がありません。

delimiters を変える手は、読む側にも同じ設定が要ります

検索するとまず出てくるのが、区切り文字を = 以外にする方法です。

config = configparser.ConfigParser(delimiters=(":",))
config["settings"] = {"a=b": "value"}

書き出しは通ります。

[settings]
a=b : value

しかしこのファイルを、既定の ConfigParser で読むと壊れます。

back = configparser.ConfigParser()          # delimiters を指定していない
back.read_string("[settings]\na=b : value\n")
print(dict(back["settings"]))
# → {'a': 'b : value'}      キー a=b はやはり消える

ConfigParser の既定の区切り文字は ('=', ':') の 2 つです(実測)。: で書き出しても、既定の読み手は行の中で先に現れる = で割ります。

したがって delimiters=(":",) が成立するのは、書く側と読む側の両方を自分が持っていて、両方に同じ設定を渡せる場合だけです(そのときは {'a=b': 'value'} に復元できます・実測)。設定ファイルを他のツールや他の言語に読ませているなら、この手は使えません。

何が拒否され、何が拒否されないか

Python 3.14 の検査が見るのは、キーが (1) その ConfigParser に設定された区切り文字を含む か、(2) セクション見出し [...] に見える形で始まる か、の 2 つです。実測です(既定の区切り文字 ('=', ':') の場合)。

書くもの3.14
キー a=bInvalidWriteErrorcontains delimiter =
キー a:bInvalidWriteError: も既定の区切り文字)
キー [section]InvalidWriteErrorbegins with section pattern
キー [section] suffixInvalidWriteError(同上)
キー x[y](先頭が [ でない)通る
キー a b(空白)/a#ba;b通る
キー a\nb(改行を含む)通る(ただし下記)
セクション名 [a=b] / [a:b]通る(検査はキーだけ)
a=b通る

区切り文字は「=:」に固定ではありません。 ConfigParser(delimiters=("|",)) を使っていれば、拒否されるのは | を含むキーで、= を含むキーは通ります(実測)。自分のパーサに設定した区切り文字に読み替えてください。

改行を含むキーは検査を通りますが、書き出した INI は結局読み戻せません。 {"a\nb": "value"}a\nb = value と書かれ、2 行に割れます。検査は区切り文字とセクション見出しだけを見るので、改行はすり抜けますが「安全」ではありません。3.13 以前は上のすべてが通ります。

すでに壊れているファイルは、3.14 でも黙って読まれます

止まるのは書き込みだけです。3.13 が生成した a=b = value を 3.14 で読むと、エラーにならず {'a': 'b = value'} が返ります(実測)。

ディスク上のファイルは、3.14 に上げても直りません。 書き出す側を直したあと、生成済みのファイルを見直してください。

予告は 1 度も出ていません

Python 3.14 の他の変更(asyncio の暗黙ループ、sqlite3 の名前つきプレースホルダ、tarfile の展開フィルタ)は、3.12 か 3.13 で DeprecationWarning を出し、多くは落ちる版まで名指ししています。

この件には警告がありません。 3.12 でも 3.13 でも、-W always::DeprecationWarning を付けても何も出ません(実測)。

つまり「3.13 で -W を付けて 3.14 の断層を洗い出す」という手順では、この件は見つかりません。3.14 で実際に書き出すまで分かりません。

切り分け

  • エラー文が contains delimiterbegins with section pattern → 前者はキーが区切り文字を含む、後者はキーが [...] の見出しに見える形です。Cannot write key ... に実際のキーが出ます。
  • キーに区切り文字も見出しも無いのに出る → キーが外部入力(環境変数名、フォームの項目名、外部 API のフィールド名)から来ていないか、そして自分の ConfigParserdelimiters= を設定していないか確認してください。
  • 読み込みでは落ちず、書き込みでだけ落ちる → 正常です。3.14 が検査するのは書き込みだけです。
  • delimiters を変えたら書けたが、別のツールが読めなくなった → 上の節のとおりです。区切り文字は書く側と読む側の両方の設定です。
  • 他の Python 3.14 のエラーを踏んだ → 同じ版境界の姉妹記事があります。sqlite3 の名前つきプレースホルダ は逆に、3.12・3.13 の警告文が落ちる版を名指ししていた例です。

検証環境

  • python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1(Python 3.14.6)、ネットワーク不要
  • 再現:既定の ConfigParser にキー a=b を入れて write() し、configparser.InvalidWriteError: Cannot write key a=b; contains delimiter = が出て終了コード 1
  • 修正(機械検証したもの):ConfigParser(delimiters=(":",)) にして同じ Python 3.14.6 で終了コード 0・シグネチャ消滅。ただしこの修正は読む側にも同じ設定が要ることを別途実測したので、記事では条件つきの手として扱い、キーから区切り文字を外す方を先に置きました
  • 3.13 が書き出した a=b = value を既定の ConfigParser で読むと {'a': 'b = value'} になること、delimiters=(":",) で書いたファイルも既定の読み手では壊れること、拒否されるのは「設定した区切り文字を含むキー」と「[...] の見出しに見えるキー」であること(delimiters=("|",) なら | を含むキーが対象・[section] で始まるキーも begins with section pattern で拒否)、改行キーは検査を通るが読み戻せないこと、3.12・3.13 では -W always::DeprecationWarning を付けても警告が出ないことは、python:3.12-slim(3.12.13)・python:3.13-slim(3.13.14)・3.14.6 の 3 版でそれぞれ実行して確認しました
  • 3.12 と 3.13 の間、3.13 と 3.14 の間のパッチ版は測っていません

検証(machine-verified)

この修正は python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 のバージョン固定コンテナ内で再現し、修正後に configparser.InvalidWriteError: Cannot write key a=b; contains delimiter = のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs python/configparser-invalid-write-python-314
● reproduce configparser.InvalidWriteError: Cannot write key a=b; contains delimiter = present ✓
● apply fix exit 0
● re-run configparser.InvalidWriteError: Cannot write key a=b; contains delimiter = gone ✓
PASS verified · python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。