同梱するPythonスクリプトの依存ライセンスを調べた。License欄だけ見ると4件が空だった

12個の依存パッケージを調べて、4個が「ライセンス不明」と出ました。どれも誰もが使う定番です。不明のはずがありません。直すべきは依存ではなく、私の検査のほうでした。
デジタル商品にPythonスクリプトを付けるとき、importが1行増えるたびに、他人のライセンス条件も商品に付いてきます。AIに下書きさせたコードでも、自分で書いたコードでも同じです。請求書のExcelやWordを整えるスクリプトを想定した見本の環境で、同梱前に何を確かめられるかを試しました。実在の商品や販売実績の話ではありません。
見本の環境を作ります
依存は、requests、chardet、pyyaml、python-docx、openpyxlの5つです。空のディレクトリに仮想環境を作り、入れました。Pythonは3.13.5です。
python3 -m venv .venv
. .venv/bin/activate
pip install -q requests chardet pyyaml python-docx openpyxl
-qを付けたので、出力はありません。ただ、推移的依存(入れたパッケージが内部で必要とするパッケージ)も一緒に入ります。結果として、pipを除いて12個がそろいました。
最初の版はLicense欄だけを見ました
メタデータにはLicenseという欄があるので、そこが空かどうかを数える短いスクリプトを書きました。
check_v1.py の全文(13行)
from importlib.metadata import distributions
blank = []
for d in distributions():
name = d.metadata["Name"]
if name == "pip":
continue
if not d.metadata.get("License"):
blank.append(name)
print("License欄が空:", len(blank), "件")
for n in sorted(blank, key=str.lower):
print(" -", n)
保存名はcheck_v1.pyです。実行すると、こうなりました。
python check_v1.py
License欄が空: 4 件
- chardet
- idna
- typing_extensions
- urllib3
4件。urllib3が空なのは変です。requestsが必ず使うパッケージだからです。そこで、4件それぞれのメタデータ全体を見ました。chardetにはLicense-Expression: 0BSD、idnaにはBSD-3-Clause、typing_extensionsにはPSF-2.0、urllib3にはMITが入っていました。
欄が空なのではありません。書く場所が違っただけでした。
3つの欄を順に読む版に直しました
PEP 639は、ライセンスをSPDX形式の式(MITやBSD-3-Clauseのような短い識別子)で書くLicense-Expression欄を追加しました。同時に、従来のLicense欄とライセンス用のclassifierは非推奨にしています。今回の12個には、新しい欄に書くパッケージと従来の欄に書くパッケージが混ざっていました。1つの欄だけ読むと、どちらかを取りこぼします。
読む順番は、License-Expression、License、classifierです。見つけた文字列を3つに分けます。
- STOP: LGPL、GPL、AGPLを含むもの。同梱の前に人が止まります
- REVIEW: MPL、EPLなど。同梱できるかもしれませんが、人が条件を読みます
- OK: MIT、BSD、Apache、PSFなど
どれにも当たらない文字列と、どの欄にも値がないものはUNKNOWNです。STOPかUNKNOWNが1件でもあれば、終了コード2で終わります。
check_licenses.py の全文(45行)
import re
import sys
from importlib.metadata import distributions
# 左から順に最初に当たった分類を使う(LGPL と AGPL を GPL より先に見る)
RULES = [
("STOP", r"AGPL|AFFERO|LGPL|LESSER|\bGPL|GENERAL PUBLIC"),
("REVIEW", r"MPL|MOZILLA|EPL|ECLIPSE|CDDL"),
("OK", r"\bMIT\b|0BSD|BSD|APACHE|PSF|PYTHON SOFTWARE|\bISC\b"),
]
def declared(meta):
"""License-Expression -> License -> Classifier の順で最初に見つかった値を返す"""
if meta.get("License-Expression"):
return "License-Expression", meta["License-Expression"]
if meta.get("License"):
return "License", meta["License"].splitlines()[0]
classes = [c for c in (meta.get_all("Classifier") or []) if c.startswith("License ::")]
if classes:
return "Classifier", classes[0].split(" :: ")[-1]
return "-", ""
def classify(text):
t = text.upper()
for label, pattern in RULES:
if re.search(pattern, t):
return label
return "UNKNOWN"
stop = False
print(f"{'name':20} {'ver':9} {'source':19} {'declared':32} verdict")
for d in sorted(distributions(), key=lambda d: d.metadata["Name"].lower()):
name = d.metadata["Name"]
if name == "pip":
continue
src, text = declared(d.metadata)
verdict = classify(text)
if verdict in ("STOP", "UNKNOWN"):
stop = True
print(f"{name:20} {d.version:9} {src:19} {text[:30]:32} {verdict}")
sys.exit(2 if stop else 0)
同じ環境で動かします。
python check_licenses.py
echo "exit=$?"
name ver source declared verdict
certifi 2026.10.23 License MPL-2.0 REVIEW
chardet 7.7.0 License-Expression 0BSD OK
charset-normalizer 3.5.2 License MIT OK
et_xmlfile 2.0.0 License MIT OK
idna 3.20 License-Expression BSD-3-Clause OK
lxml 6.1.3 License BSD-3-Clause OK
openpyxl 3.1.5 License MIT OK
python-docx 1.2.0 License MIT OK
PyYAML 6.0.3 License MIT OK
requests 2.34.2 License Apache-2.0 OK
typing_extensions 4.16.0 License-Expression PSF-2.0 OK
urllib3 2.8.0 License-Expression MIT OK
exit=0
空に見えた4件は、すべてLicense-Expressionから読めました。止まるものはありません。ただしcertifiはMPL-2.0で、REVIEWのまま残っています。人が見る対象を1件に減らしただけ。それがこの検査の成果です。
古いchardetの環境なら止まるか
検査が通っただけでは、検査が働いている証拠になりません。止まる入力でも試します。chardetを5.2.0に固定した別の環境です。
python3 -m venv .venv-old
. .venv-old/bin/activate
pip install -q requests "chardet==5.2.0"
python check_licenses.py
echo "exit=$?"
name ver source declared verdict
certifi 2026.10.23 License MPL-2.0 REVIEW
chardet 5.2.0 License LGPL STOP
charset-normalizer 3.5.2 License MIT OK
idna 3.20 License-Expression BSD-3-Clause OK
requests 2.34.2 License Apache-2.0 OK
urllib3 2.8.0 License-Expression MIT OK
exit=2
chardetは、5.2.0がLGPL、7.7.0が0BSDと宣言していました。パッケージ名が同じでも、版でライセンスが変わっています。商品に同梱するなら、requirements.txtで版を固定し、その版で検査するのが筋だと考えています。購入者の環境で後から更新されても、検査したときの条件は変わらないからです。
まだ納得していないこと
この検査には、わかっている穴が3つあります。
1つ目は、メタデータは作者の自己申告だという点です。宣言と実際のライセンス本文が食い違っていても、この検査は気づきません。2つ目は、分類が正規表現の文字列一致で、粗いことです。「GPL互換のMIT」のような文があれば、誤ってSTOPにするでしょう。3つ目は、REVIEWの判断を機械に任せていないことです。certifiのMPL-2.0を同梱してよいかは、私にはまだ決められません。本文を読む必要があります。
それでも、4件が空に見えた最初の版よりは、人の読む場所が絞れました。十分でしょうか。まだ足りません。次は、wheel(配布用のzip形式のファイル)にライセンス本文が入っているかを数える検査を足してみます。


