Error [ModuleNotFoundError: No module named 'cgi']

ModuleNotFoundError: No module named 'cgi' の直し方(Python 3.13 が標準ライブラリから削除した)

FIX SUMMARY verified
Applies when
python:3.13.14-slimPEP 594 removed cgi (and crypt/telnetlib/imghdr/pipes/nntplib) from the stdlib in Python 3.13

Verified: reproduced in python:3.13.14-slim, then the ModuleNotFoundError: No module named 'cgi' signature was gone after the fix (exit 0).

Python 3.12 まで動いていたコードを 3.13 で実行すると、次のエラーで落ちることがあります。

Traceback (most recent call last):
  File "app.py", line 1, in <module>
    import cgi
ModuleNotFoundError: No module named 'cgi'

cgi標準ライブラリだったモジュールです。PEP 594(通称 dead batteries)によって、Python 3.13 が標準ライブラリから削除しました

同じ理由で消えたモジュールは他にもあります。エラーの名前が違うだけで、原因も対処も同じです。

  • No module named 'cgi' / No module named 'cgitb'
  • No module named 'crypt'
  • No module named 'telnetlib'
  • No module named 'imghdr'
  • No module named 'pipes'
  • No module named 'nntplib'
  • No module named 'audioop'pydub などが使っています)
  • No module named 'mailcap' / 'aifc' / 'chunk' / 'sndhdr' / 'sunau' / 'uu' / 'xdrlib' / 'ossaudiodev' / 'spwd'

PEP 594 が 3.13 で削除したモジュールは 19 個あります。上に無い名前でも、標準ライブラリのモジュールなら PEP 594 の一覧を確認してください。

pip install cgi は存在しません

最初に、いちばん多い空振りを潰しておきます。

$ pip install cgi
ERROR: Could not find a version that satisfies the requirement cgi (from versions: none)
ERROR: No matching distribution found for cgi

モジュール名と同じ名前の pip パッケージはありません。 バックポートは別の名前で公開されていて、しかも接頭辞が揃っていません

モジュールインストールするパッケージ
cgilegacy-cgicgitb も入ります)/standard-cgi
cryptstandard-crypt
telnetlibstandard-telnetlib
imghdrstandard-imghdr
pipesstandard-pipes
nntplibstandard-nntplib
audioopaudioop-lts

接頭辞は揃っていません。 多くは standard- ですが、audioop の受け皿は audioop-lts で、standard- でも legacy- でもありません(standard-audioop は存在しません)。cryptlegacy-crypt は存在せず standard-crypt です。名前を当てずっぽうで打つと外します。 PyPI で確認してから入れてください。

ただし、多くの場合はバックポートを入れずに直せます

まず、import cgi しているのは誰かを見る

トレースバックの File の行を読みます。自分のコードか、依存ライブラリの中かで、やることが変わります。

  File "/app/main.py", line 1, in <module>
    import oldlib
  File "/usr/local/lib/python3.13/site-packages/oldlib/parser.py", line 3, in <module>
    import cgi
ModuleNotFoundError: No module named 'cgi'

import cgi の 1 つ上の行が site-packages の中なら、cgi を呼んでいるのは依存ライブラリです。自分のファイルなら、書いたのはあなたです。

  • site-packages の中で import されている → 依存ライブラリが原因です。まずそのライブラリを上げてください。 3.13 に対応した版が出ていることが多く、それがいちばん確実です。上げられない事情があるときだけ、上のバックポートを入れます。
  • 自分のコードで import している → 書き直せます。次の節へ。

自分のコードの cgi を書き換える

cgi の用途で圧倒的に多いのは、parse_header()Content-Type を解釈することです。これは email.message で同じことができます。追加のインストールは要りません。

# Before(Python 3.13 で落ちる)
import cgi

header = "multipart/form-data; boundary=----abc123; charset=utf-8"
mimetype, params = cgi.parse_header(header)

print(mimetype)            # multipart/form-data
print(params["boundary"])  # ----abc123
# After(標準ライブラリのまま。3.13 で動く)
from email.message import EmailMessage

header = "multipart/form-data; boundary=----abc123; charset=utf-8"
msg = EmailMessage()
msg["content-type"] = header

mimetype = msg.get_content_type()
params = dict(msg["content-type"].params)

print(mimetype)            # multipart/form-data
print(params["boundary"])  # ----abc123

mimetypeboundarycharset のいずれも、cgi.parse_header() と同じ値になります。

ただし、ヘッダの種類に合わせて入れる場所を変えてください

cgi.parse_header() は、渡された文字列がどのヘッダのものかを知りません値; key=value; ... という形を機械的に分解するだけの、種類を問わないパーサです。

email.message は違います。ヘッダ名で意味づけをします。 だから、Content-Disposition の値を content-type に入れると、例外も警告も出さずに間違った値が返ります

# アップロードされたファイル名を取りたい(Content-Disposition)
header = 'attachment; filename="report.pdf"'

msg = EmailMessage()
msg["content-type"] = header        # 種類が違う
print(msg.get_content_type())       # text/plain ('attachment' ではない)

attachment は MIME タイプとして解釈できないので、email.message は既定値の text/plain を返します。エラーにならないぶん、そのまま通ります。

正しくは、そのヘッダ名のフィールドに入れて、対応するアクセサで読みます

header = 'attachment; filename="report.pdf"'

msg = EmailMessage()
msg["content-disposition"] = header

disposition = msg.get_content_disposition()      # attachment
params = dict(msg["content-disposition"].params) # {'filename': 'report.pdf'}

これで cgi.parse_header() と同じ値になります。Content-Type なら content-typeget_content_type()Content-Disposition なら content-dispositionget_content_disposition() です。アップロードのファイル名を取り出す用途は Content-Disposition なので、get_content_type() を使い回さないでください

RFC 2231 のパラメータは、返り方が変わります

もう 1 つ、値が変わる箇所があります。filename*=charset*= のような RFC 2231 のエンコード済みパラメータです。

header = "text/plain; charset*=utf-8''rfc2231%20encoded"

# cgi.parse_header
#   {'charset*': "utf-8''rfc2231%20encoded"}   キーは charset*、値は生のまま
# email.message
#   {'charset': 'rfc2231 encoded'}             キーは charset、値はデコード済み

キーも値も違います。 email.message のほうが RFC どおりにデコードしてくれるので結果としては望ましいのですが、params["charset*"]params["filename*"] を直接読んでいるコードは、キーが存在せず KeyError になります。星付きのキーを触っている箇所は、書き換えのときに確認してください。

cgi.FieldStorage を使っている場合

cgi.FieldStorage(multipart のリクエスト本体そのものを解析する機能)は、email.message では置き換えられません。 そちらを使っているなら、Web フレームワーク側のリクエスト解析(Django や Flask が持っています)か、multipart などの専用パッケージに寄せることになります。email.message で置き換えられるのは、ヘッダの解釈までです。

「警告なんて出ていなかった」の正体

このエラーで理不尽に感じるのは、予告が無かったように見えることです。実際には予告されていました。

Python 3.11 と 3.12 で import cgi すると、こう出ます。

DeprecationWarning: 'cgi' is deprecated and slated for removal in Python 3.13

削除される版まで名指ししています。 それでも見えなかったのは、Python の既定の警告フィルタが DeprecationWarning を隠すからです。既定では、あなた自身のトップレベルのスクリプトから出たものしか表示されません

  • 自分のコードが import cgi している → 3.12 で警告が見える。
  • 依存ライブラリの中が import cgi している → 3.12 では警告が 1 行も出ない。 何事もなく動きます。

そして 3.13 に上げた瞬間、いきなり ModuleNotFoundError になります。 警告は出ていましたが、既定のフィルタに隠されて画面に届いていませんでした。

3.12 のうちに、まとめて見つける

上げてから 1 つずつ踏むより、3.12 の環境で隠れている予告をあぶり出すほうが安全です。警告フィルタを明示すれば、依存ライブラリの中のものまで出ます。見つかるのは、そのコマンドで実際に import される経路の警告だけなので、テストが通らないコードパスは漏れます。

# 隠れている DeprecationWarning を全部表示する
python -W always::DeprecationWarning main.py
/usr/local/lib/python3.12/site-packages/oldlib/parser.py:3: DeprecationWarning:
  'cgi' is deprecated and slated for removal in Python 3.13
  import cgi

CI に組み込むなら、警告をエラーに昇格させて落とします

python -W error::DeprecationWarning -m pytest

こうすると終了コードが 0 以外になり、トレースバックが import の連鎖(main.py → 依存ライブラリ → cgi)を示します。3.13 に上げる前に、テストが通る範囲で壊れる箇所が見つかります。

どのモジュールがどの版で消えたか

「3.13 で消えた」で一括りにすると外します。削除は 3.12 と 3.13 の 2 回に分かれています。

モジュール3.123.13根拠
distutils消えている消えているPEP 632
asynchat / asyncore / smtpd消えている消えているPEP 594
cgi / crypt / telnetlib / audioop / ほか残っている(警告あり)消えているPEP 594

distutils は PEP 632 という別の PEP による削除で、PEP 594 とは根拠が違います。一方 PEP 594 自身も 3.12 で 3 つ削除していますasynchat / asyncore / smtpd)。

「3.12 に上げたときは cgi で何も起きなかった」のは正常です。3.12 の番と 3.13 の番が違うだけで、同じ PEP 594 の続きです。

切り分け

  • pip install cgiNo matching distribution found になる → 正常です。そのパッケージは存在しません。legacy-cgi を入れるか、email.message に書き換えてください。
  • バックポートを入れたのに直らない → パッケージ名を取り違えている可能性があります。cryptlegacy-crypt ではなく standard-cryptaudioopstandard-audioop ではなく audioop-lts です。
  • ModuleNotFoundError: No module named 'distutils' → 同じ「標準ライブラリから消えた」形ですが、消えたのは 3.12 で、根拠の PEP も対処も違います。No module named ‘distutils’ の直し方 を参照してください。
  • 手元では動くのに CI やコンテナでだけ落ちる → Python の版が違います。python -V を両方で確認してください。3.13 のイメージに上げた直後に出るのが典型です。
  • 書き換えたら、ファイル名は取れるのに MIME タイプが text/plain になったContent-Disposition の値を content-type フィールドに入れています。content-disposition に入れて get_content_disposition() で読んでください。例外は出ません。
  • params["filename*"]KeyError になったemail.message は RFC 2231 のパラメータをデコードして、星の無いキー(filename)で返します。星付きのキーは存在しません。
  • cgi ではなく cgitb → こちらも同時に削除されています。cgitblegacy-cgi に含まれます。
  • エラーが ImportError: cannot import name ... → モジュール自体はあるので、これは削除ではなく、そのモジュールの中身が変わったときの形です。別件です。

検証環境

  • python:3.13.14-slim、ネットワーク無し
  • 再現:cgi.parse_header()Content-Type を解釈するスクリプトを実行し、ModuleNotFoundError: No module named 'cgi' が出て終了コード 1
  • 修正:同じ処理を email.message.EmailMessage で書き直すだけで終了コード 0・シグネチャ消滅。バックポートは入れていません(ネットワーク無しで通ります)。mimetype / boundary / charset の 3 点が再現側と同じ値になることも確認しました
  • 削除されたモジュールの一覧(crypt / cgi / telnetlib / imghdr / pipes / nntplib が 3.12 では import でき、3.13 ではできない。distutils だけ 3.12 で既に消えている)は、python:3.12-slimpython:3.13-slim でそれぞれ確認しました
  • バックポートの表(legacy-cgi / standard-cgi / standard-crypt / standard-telnetlib / standard-imghdr / standard-pipes / standard-nntplib / audioop-lts)は、python:3.13-slim で 1 つずつ pip install してから import が通ることを確認しました。pip install cgi / legacy-crypt / standard-audioop が存在しないことも確認しています
  • 削除の版が 3.12 と 3.13 に分かれていること(asynchat / asyncore / smtpd / distutils は 3.12、それ以外の PEP 594 対象は 3.13)は、python:3.11-slim / 3.12-slim / 3.13-slimimport を回して確認しました
  • 「3.11 / 3.12 では DeprecationWarning が出る」「依存ライブラリの中からの import では既定では表示されない」「-W always::DeprecationWarning で見える」「-W error::DeprecationWarning で終了コード 1 になる」も、それぞれ実行して確認しました
  • cgi.parse_header()email.message の食い違いContent-Dispositioncontent-type に入れると text/plain が返る/content-disposition に入れて get_content_disposition() で読めば一致する/RFC 2231 のパラメータは charset* の生値ではなく charset のデコード済みの値になる)は、python:3.12-slim で両者に同じヘッダを渡して 1 件ずつ突き合わせて確認しました

cgi.FieldStorage の置き換え先(Web フレームワークのリクエスト解析や multipart パッケージ)は確認していません。依存ライブラリを上げれば直るかどうかも、ライブラリごとに異なるため確認していません。crypt などバックポート以外の代替も、この記事では確認していません(実測したのは cgi.parse_header の置き換えのみです)。

検証(machine-verified)

この修正は python:3.13.14-slim のバージョン固定コンテナ内で再現し、修正後に ModuleNotFoundError: No module named 'cgi' のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs python/cgi-module-removed-python-313
● reproduce ModuleNotFoundError: No module named 'cgi' present ✓
● apply fix exit 0
● re-run ModuleNotFoundError: No module named 'cgi' gone ✓
PASS verified · python:3.13.14-slim · signature gone