【Python】Python 3.15 移行ログ — 3.14 から上げて壊れた箇所と直し方

Python 3.15 移行ログを表したアイキャッチ画像
目次

Python 3.15 に上げると何が壊れるのか

  • Python 3.15 に上げたら何が壊れるのか、手を動かした結果で知りたい
  • sqlite3 や argparse など、地味なところで出るエラーの直し方が知りたい
  • lazy import は便利そうだけど、どこで使えるのかあやふや

対象読者: Python 3.14 以前のプロジェクトを 3.15 に上げる技術者。

前提知識: Python の基本構文と venv(または uv)での実行環境の作り方。

結論

3.15 で確実にエラーになるのは sqlite3.connect() の位置引数 と argparse の dest 推定 です。両方とも 3.14 が警告を出して教えてくれます。

# 影響箇所の洗い出しはこれで。EncodingWarning が1つも出なければ UTF-8 化の影響は無し
python -X warn_default_encoding -m pytest

# sqlite3 は位置引数をやめてキーワード引数に(3.14 の時点でやっておけば壊れない)
sqlite3.connect("app.db", timeout=5.0)

sqlite3.connect() の位置引数がエラーになった

3.14 では DeprecationWarning が出るだけでした。これが 3.15 では TypeError になります。

import sqlite3

conn = sqlite3.connect("app.db", 5.0)  # 2番目の位置引数は timeout

# 実行結果(3.14.7)
# DeprecationWarning: Passing more than 1 positional argument to sqlite3.connect()
# is deprecated. Parameters 'timeout', 'detect_types', ... will become
# keyword-only parameters in Python 3.15.

同じコードを 3.15 で動かすとこうなります。エラー内容が素直なので直す箇所はすぐ分かりますね。

sqlite3: 3.53.4
位置引数 connect(db, 5.0): connect() takes at most 1 positional arguments (2 given)
キーワード引数 connect(db, timeout=5.0): OK

lazy import(PEP 810)

新しい lazy キーワードを import の前に置くと、モジュールのロードを初回使用時まで遅らせられます。まずは遅延の実測から。

import sys

lazy import json

print("宣言直後 json はロード済みか:", "json" in sys.modules)

data = json.dumps({"lang": "ja"})
print("使用後 json はロード済みか:", "json" in sys.modules, "| dumps:", data)

# 実行結果
# 宣言直後 json はロード済みか: False
# 使用後 json はロード済みか: True | dumps: {"lang": "ja"}

注意点は2つです。lazy はモジュールのトップレベルでしか書けないこと。関数内に書くと構文エラーで止まります。

  File "probe_lazy_fn.py", line 2
    lazy import json
    ^^^^^^^^^^^^^^^^
SyntaxError: lazy import not allowed inside functions

もう1つは、ロードが遅延した分、失敗も初回使用時まで遅れること。モジュールが存在しない場合、import 文の行ではなく最初に名前を触った行で例外が出ます。ソースを一括で lazy 化するときは、この2点を先に確認しておくと安心です。

ソースを書き換えずに全部の import を遅延させる -X lazy_imports=all(または環境変数 PYTHON_LAZY_IMPORTS)もあります。import 時に副作用のあるモジュールまで遅れるので、本番でいきなり all にするのではなく、まず lazy キーワードで重い所だけ指定するのが安全ですね。

UTF-8 が既定のエンコーディングに(PEP 686)

encoding 引数を省略した I/O が、システムのロケールではなく常に UTF-8 になります。影響が出るのは主に Windows で、レガシーなコードページ(cp932 など)の環境で Shift-JIS のファイルを読むコードです。

この検証機は Windows の ANSI コードページが 65001(UTF-8)の環境なので、3.14 と 3.15 の間で挙動は変わりませんでした(どちらも open(path).encoding は utf-8)。差が出るのは cp932 の環境です。

影響箇所の調べ方は、3.15 に上げる前から -X warn_default_encoding を付けてテストを回すこと。encoding を省略した箇所が全部 EncodingWarning として出ます。

C:\verify\probe_warn.py:1: EncodingWarning: 'encoding' argument not specified
  with open("legacy.txt", "w") as f:
C:\verify\probe_warn.py:3: EncodingWarning: 'encoding' argument not specified
  print("encoding:", open("legacy.txt").encoding)
encoding: utf-8

どうしても旧挙動が必要なときは PYTHONUTF8=0 で全体を戻すか、呼び出しごとに encoding="locale" を付けます。ただし両方ともその場しのぎにすぎないので、警告が出た箇所から encoding を明示していくのが本筋です。

frozendict と sentinel

組み込みに frozendict と sentinel が入りました。frozendict は dict のサブクラスではないので、isinstance(x, dict) で判定しているコードは直す必要があります。

fd = frozendict({"x": 1, "y": 2})
print("dict のサブクラスか:", isinstance(fd, dict), "| hash 可能か:", isinstance(hash(fd), int))

try:
    fd["x"] = 99
except TypeError as e:
    print("代入:", e)

# 実行結果
# dict のサブクラスか: False | hash 可能か: True
# 代入: 'frozendict' object does not support item assignment

sentinel は「値が指定されたかどうか」を判別するための専用型です。デフォルト引数に None を使うと「None を指定した」と「指定しない」を区別できませんでしたが、これがきれいに書けます。

MISSING = sentinel("MISSING")

def get(config, key, default=MISSING):
    if default is MISSING:
        print(key, "-> 未指定(KeyError 相当の扱い)")
        return None
    return config.get(key, default)

print(get({"a": 1}, "a", 0))
print(get({}, "b", 0))
get({}, "b")

# 実行結果
# 1
# 0
# b -> 未指定(KeyError 相当の扱い)

argparse の dest 推定が変わった

add_argument("-f", "-foo") のように単一ダッシュの長いオプションを混ぜると、3.15 から dest は短い方(f)ではなく foo になります。Namespace の属性名が変わるので、3.14 まで動いていたコードが AttributeError で落ちます。

# 3.14.7
AttributeError: 'Namespace' object has no attribute 'foo'

# 3.15.0
Namespace の属性: x

直し方は対象の引数に dest= を明示すること。Python 3.15 の Porting セクションにも同じ対処が書かれています。

つまずきポイント

  • sqlite3: 位置引数は 3.15 で TypeError。3.14 の DeprecationWarning が実質的な移行告知
  • lazy import: 関数内・クラス本体・try ブロックでは SyntaxError。失敗の発生箇所も遅延する
  • frozendict: dict のサブクラスではない。isinstance で弾かれているコードがある
  • argparse: 単一ダッシュの長いオプションで dest が変わる。dest を明示して固定する
  • UTF-8 既定化: ANSI コードページが UTF-8 の Windows では挙動は変わらない。cp932 環境でだけ差が出る

検証環境

OS Windows 11 (build 10.0.26200)
CPU AMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics
メモリ 28GB
ツール CPython 3.15.0 / CPython 3.14.7

上記の環境で実際に実行して確認した結果です。各セクションのコードブロックと出力は、この実測からの抜粋です。

Python 3.15.0 と 3.14.7 で移行影響を確認した実測ログ。sqlite3.connect の位置引数、lazy import の遅延と関数内 SyntaxError、warn_default_encoding、frozendict・sentinel、argparse の dest 推定の差までを1枚にまとめたもの
Python 3.15.0 と 3.14.7 で移行影響を確認した実測ログ。sqlite3.connect の位置引数、lazy import の遅延と関数内 SyntaxError、warn_default_encoding、frozendict・sentinel、argparse の dest 推定の差までを1枚にまとめたもの

まとめ

3.15 の移行で必ず見るのは sqlite3・argparse・encoding の3点です。どれも 3.14 で警告か lint で拾えるので、3.15 に上げる前に -X warn_default_encoding 付きのテストを1回回しておくのが最短ルートだと思いますよ。

参考

【広告】【Value AI Writer byGMO】高品質なSEO記事をAIで自動生成。

あわせて読みたい

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次