この記事は 3.8〜3.15 を横断して比較したうえで、個人的に 3.9 と 3.11 をおすすめする理由を書いている。「最新が正義」でも「安定が正義」でもなく、選択の根拠を整理したくて書いた。あくまで個人的な見解なので、異論は歓迎。
サポートスケジュールの全体像
選択の前提として、まずどのバージョンがいつまで生きているかを把握しておく。
3.8 は 2024年10月、3.9 は 2025年10月にそれぞれ EOL を迎えている。セキュリティパッチも届かないため、新規プロジェクトへの採用は推奨できない。既存プロジェクトを維持しているケースでは話が別だが、移行計画は立てておいた方がいい。
パフォーマンスの変遷
数字で見ると、3.11 がいかに特異な転換点だったかが分かる。
| バージョン | ベンチマーク相対値 | 対 3.9 比 | 備考 |
|---|---|---|---|
| 3.8 | 96 | -4% | 3.9 より若干遅い |
| 3.9 | 100 | 基準 | — |
| 3.10 | 99 | -1% | 誤差レベル。体感差なし |
| 3.11 | 129 | +29% | CPython 最適化の大転換 |
| 3.12 | 141 | +41% | インタープリタ改善が継続 |
| 3.13 | 149 | +49% | 実験的 JIT 搭載 |
| 3.13 (JIT) | 157 | +57% | PYTHON_JIT=1 で有効化 |
| 3.14 | ~155〜165 | +55〜65% | t-strings・アノテーション遅延評価 |
| 3.15 | 未計測 | — | 開発中 |
3.10 が 3.9 とほぼ同スコアという事実は地味に重要で、「バージョンを上げれば速くなる」という思い込みが崩れる一例だ。3.11 の +29% というのは pyperformance ベースの平均値で、Ryzen 9 5950X での実測でも同様の結果が出ている。
バージョン別の主要変更点
3.8(2019年10月)
# セイウチ演算子(:=)— 条件式の中で代入できる
import re
data = "Error: Connection refused"
if m := re.search(r"Error: (.+)", data):
print(m.group(1)) # Connection refused
# 位置専用引数(/)
def greet(name, /, greeting="Hello"):
return f"{greeting}, {name}"
# f-string デバッグ記法
value = 42
print(f"{value=}") # value=42
3.8 はすでに EOL だが、セイウチ演算子と f-string デバッグ記法は現在も広く使われている。これらを知らずに 3.7 以前のコードを読むと混乱するので、把握しておく価値はある。
3.9(2020年10月)
# 組み込み型でジェネリクスが使えるようになった(typing 不要)
def process(items: list[int]) -> dict[str, int]:
return {str(i): i for i in items}
# 辞書の統合演算子
defaults = {"timeout": 30, "retry": 3}
overrides = {"timeout": 60}
config = defaults | overrides # {"timeout": 60, "retry": 3}
# str のメソッド強化
" hello ".removeprefix(" ") # "hello "
"hello world".removesuffix(" world") # "hello"
list[int] が使えるようになったのが地味に大きい。3.8 以前の from typing import List から解放される最初のバージョンがここだ。
3.10(2021年10月)
# Structural Pattern Matching
def classify_http_status(status):
match status:
case 200:
return "OK"
case 404:
return "Not Found"
case code if 500 <= code < 600:
return f"Server Error ({code})"
case _:
return "Other"
# エラーメッセージの改善(括弧の対応漏れを指摘してくれる)
# SyntaxError: '{' was never closed ← 3.10以降
match/case は Rust の pattern matching に触れたことがある人には馴染みやすい。ただ、3.9 以前との共存プロジェクトに使うと当然動かなくなるので注意。
3.11(2022年10月)
# トレースバックがエラー箇所を ^ で正確に示す
# 3.11以前: どの変数が None かよくわからない
# 3.11以降:
# result = obj.method().attribute
# ^^^^^^^^^^^
# AttributeError: 'NoneType' object has no attribute 'attribute'
# tomllib が標準ライブラリに追加(pip install 不要)
import tomllib
with open("pyproject.toml", "rb") as f:
config = tomllib.load(f)
print(config["project"]["name"])
# ExceptionGroup — 複数の例外をまとめて扱う
try:
raise ExceptionGroup("検証エラー", [
ValueError("email が不正"),
TypeError("age は整数が必要"),
])
except* ValueError as eg:
for e in eg.exceptions:
print(f"値エラー: {e}")
3.11 の改善されたトレースバックは、一度体験すると以前には戻れない。デバッグにかかる時間が体感でかなり変わる。
3.12(2023年10月)
# f-string の制限が大幅に緩和(ネスト・バックスラッシュ・コメント可)
data = {"user": "alice", "score": 98}
msg = f"ユーザー: {data["user"]}, スコア: {data["score"] * 1.5:.1f}"
# 型パラメータ構文(TypeVar が不要に)
def first[T](lst: list[T]) -> T:
return lst[0]
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
# @override デコレータ
from typing import override
class MyLogger(BaseLogger):
@override
def log(self, message: str) -> None: # オーバーライドのタイポを静的に検出
super().log(f"[INFO] {message}")
3.12 から sys.monitoring が導入されるなど C API の整理が進み、古い C 拡張が壊れるケースが増えた。NumPy や pandas は対応済みだが、ニッチな科学計算ライブラリや社内パッケージでビルドエラーが出るケースがある。依存関係の確認は必須。
3.13(2024年10月)
# 実験的フリースレッドモード(GIL フリー)
# python3.13t として別ビルドが提供される
import threading
results = []
lock = threading.Lock()
def cpu_bound(n):
total = sum(i * i for i in range(n))
with lock:
results.append(total)
# GIL フリーで真の並列 CPU 実行(実験的)
threads = [threading.Thread(target=cpu_bound, args=(10**6,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
# JIT コンパイラ(実験的)
# PYTHON_JIT=1 python3.13 script.py
GIL フリーと JIT はどちらも「将来への布石」という段階で、本番環境への採用はまだ早い印象がある。ただし CPython の方向性として明確に舵を切ったので、3.14・3.15 で安定してくる可能性が高い。
3.14(2025年10月7日)
t-strings の核心は「即時評価しない」ことにある。f-string が常に文字列を返すのに対し、t-string は Template オブジェクトを返す。これにより、ライブラリ側でサニタイズ処理を挟める設計が可能になった。
# t-strings(PEP 750)— f-string との違いは「即時評価しない」こと
name = "Alice; DROP TABLE users;"
# f-string: そのまま文字列に展開される(SQL インジェクション危険)
query_f = f"SELECT * FROM users WHERE name = '{name}'"
# t-string: Template オブジェクトを返す(処理を委譲できる)
query_t = t"SELECT * FROM users WHERE name = '{name}'"
# Template の構造を確認する
from string.templatelib import Template
for part in query_t:
print(type(part), repr(part))
# <class 'str'> "SELECT * FROM users WHERE name = '"
# <class 'string.templatelib.Interpolation'> Interpolation('Alice; ...', ...)
# <class 'str'> "'"
t-strings は f-string の代替ではなく、「サニタイズが必要な文脈でのフォーマット」に特化した機能だ。SQL クライアントや HTML テンプレートエンジンが内部で活用するイメージが近く、日常的な文字列フォーマットで使うものではない。
もう一つの注目点として、Zstandard 圧縮モジュール zstd が標準ライブラリに追加された。bz2 や lzma と同じ感覚で使えるようになる。
# zstd が標準ライブラリに(3.14以降、pip install 不要 / PEP 784)
import compression.zstd as zstd
data = b"compress this " * 1000
compressed = zstd.compress(data)
decompressed = zstd.decompress(compressed)
また、アノテーションの遅延評価(PEP 749)により from __future__ import annotations が不要になる方向も進んでいる。
# 前方参照が自然に書けるようになる
class Node:
def __init__(self, children: list[Node]): # 3.14以前は NameError になりうる
self.children = children
3.15(2026年10月予定)
PEP 790 によると、3.15 の正式リリースは 2026年10月1日予定で現在はアルファ段階にある。注目している確定機能はこれ。
# frozendict — PEP 814(2026年2月 Steering Council 承認済み)
# 組み込み型として追加予定
config = frozendict({"host": "localhost", "port": 5432})
config["host"] # "localhost" — 読み取りは普通の dict と同じ
config["host"] = "x" # TypeError: 'frozendict' object does not support item assignment
hash(config) # ハッシュ可能 → set や dict のキーに使える
# frozenset の dict 版という理解が一番しっくりくる
cache = {
frozendict({"method": "GET", "path": "/api/users"}): response_data
}
frozendict は長年サードパーティで実装されてきた(frozendict パッケージ等)が、ついに組み込みに昇格する。設定オブジェクトや「変えられない辞書」を渡したいケースはそれなりに多く、待望していた人も多いはず。
PEP 2026 でカレンダーバージョニング(3.26 形式)への移行が議論されているが、2026年3月時点では提案段階であり、3.15 という名称は維持されている。
3.15 はまだ開発中のため、この記事の情報は変わる可能性がある。リリースノートは 2026年10月前後に必ず確認してほしい。
ライブラリ互換性マトリクス
| ライブラリ | 3.8 | 3.9 | 3.10 | 3.11 | 3.12 | 3.13 | 3.14 |
|---|---|---|---|---|---|---|---|
| NumPy | 最終: 1.24 | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 |
| pandas | 最終: 2.0 | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 |
| TensorFlow | 対応 | 対応 | 対応 | 対応 | 要確認 | 要確認 | 未確認 |
| PyTorch | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 | 未確認 |
| Django | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 |
| FastAPI | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 |
| tqdm | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 | 対応 |
| cryptography | 対応 | 対応 | 対応 | 対応 | 対応 | 要確認 | 未確認 |
| 古い C 拡張 | ◎ 高 | ◎ 高 | ◎ 高 | △ 中 | × 低 | × 低 | × 低 |
「要確認」「未確認」は「絶対動かない」ではなく、バージョン依存で動く可能性があるという意味。実際には pip install してみて初めてわかるケースが多い。本番投入前は必ず仮想環境で検証を。
古い C 拡張の互換性が 3.12 から一気に落ちている背景には、CPython の内部 C API 整理が本格化したことがある。科学計算系や企業向けの商用ライブラリでは、3.11 止まりのものが今でも珍しくない。
バージョン間の関係を整理する
なぜ 3.9 と 3.11 を推すのか
ここが本題だ。上の比較を踏まえて、自分の考えを改めて整理する。
3.9 を推す文脈は「枯れた環境の維持」
from tqdm import tqdm
import time
reasons_for_39 = [
"list[int] 記法が使える最初のバージョン",
"古い C 拡張がほぼ全て動く",
"Docker イメージが豊富で CI 環境に困らない",
"企業の既存プロジェクトでまだ現役が多い",
"| による辞書マージが使える",
]
for reason in tqdm(reasons_for_39, desc="3.9 を選ぶ理由"):
time.sleep(0.2)
print(f" → {reason}")
正直に言うと、すでに EOL を迎えている 3.9 を今から新規プロジェクトに採用することは絶対に勧めるべきではない。セキュリティリスクは実在するし、遅かれ早かれ移行計画は立てるべきだ。
しかし、それでも私はあえて 3.9 を「おすすめの選択肢」として推したい。
なぜなら、現実の開発現場では「バージョンを上げたせいで古い C 拡張が壊れ、業務が止まる」ことの方が、直近の致命傷になる場面が多々あるからだ。リスクを正しく認知した上で、「list[int] が使える最低限のモダンさを保ちつつ、すべての依存ライブラリが平和に動く最後の砦」として 3.9 に留まるという選択は、実務上極めて理にかなっている。
3.11 を推す文脈は「今から作るなら」
3.11 が面白いのは、このチャートで「まだそこそこ右にいながら、一気に上にジャンプしている」点だ。速度面での恩恵(+29%)を受けつつ、3.12 以降で壊れがちな古い依存関係をある程度まだ許容できる。2027年10月までセキュリティパッチが届く点も、長期運用するプロジェクトには安心感がある。
ユースケース別おすすめ
最後に結論
「3.9 は EOL だからあり得ない」「いやいや、最新の 3.14 や 3.15 を追うべきだ」——色々な意見があると思う。
しかしバージョン選択は、「新しければ正解」でも「安定していれば正解」でもないのではないだろうか。それは結局のところ、依存ライブラリ・チームの制約・EOL リスク・必要な機能、この 4 軸を自分の状況に当てはめて、泥臭く判断するのが現実的だと思っている。
この記事が、あなたの現場での「バージョン選定の根拠」を整理する材料になればうれしい。
異論・「いや 3.12 一択だろ」もコメントで待ってます。LGTM もらえると励みになります。