👀

Python3.14の新機能の実験と紹介

に公開

はじめに

Python3.14.0 が2025/10/7 にリリースされました。
この記事では新機能のサンプルコードを示しつつ紹介します。

今回の調査については以下の環境で行なっています。
そのため、途中のサンプルコードは以下の環境でのみ動作します。

Pythonの情報:

import sys
>>> sys.version_info
sys.version_info(major=3, minor=14, micro=0, releaselevel='final', serial=0)
>>> sys._is_gil_enabled()
True

マシン環境:
- macOS 15.4.1
- 2 GHz クアッドコアIntel Core i5
- 16 GB 3733 MHz LPDDR4X

Python3.14の新機能

  • annotationの遅延評価
  • マルチインタープリタ
  • Template String
  • 括弧なしの except および except* 式を許可する
  • finally区のフローの制限
  • zstdのサポート
  • CPython 用の安全な外部デバッガインターフェース
  • Asyncioイントロスペクション機能

詳細については下記のドキュメントを参照してください。
https://www.python.org/downloads/release/python-3140/
https://docs.python.org/3.14/whatsnew/3.14.html

annotationの遅延評価

annotationは変数や関数のメタ情報をつける仕組みで、型ヒントなどで利用されます。
たとえばPython3.13などでは以下のように変数や関数、クラスのannotationを確認できます。

x :int = 3
y: list[str] = ["a", "b"]
print("__annotations__", __annotations__)
def y(a: str, b:int)->list[str]:
  return []
print("y", y.__annotations__)
class C:
  a: int = 1
  b: int = 2
print("C", C.__annotations__)

このannotationが遅延評価されるようになり、以下のような実装が正常に動作するようになりました。

def f(x: Cls) -> None: ...
class Cls: pass

# 3.14 の遅延評価では:
# {'x': <class 'Cls'>, 'return': None}
print(f.__annotations__)

python3.13以前ではNameError: name 'Cls' is not definedとなり、正しく動かすには次のように宣言する必要があります。

def f(x: 'Cls') -> None: ...
class Cls: pass

また、__annotations__の変わったため一部挙動が変わっています。たとえば、python3.14で以下のコードを動かすとname '__annotations__' is not defined.となります。[1]

x :int = 3
y: list[str] = ["a", "b"]
print("__annotations__", __annotations__)

__annotations__を使用するかわりにannotationlibを使用してannotationの情報を取得することが推奨されています。

from annotationlib import get_annotations
import sys
x :int = 3
y: list[str] = ["a", "b"]
print(get_annotations(sys.modules[__name__]))

def y(a: str, b:int)->list[str]:
  return []
print(get_annotations(y))
class C:
  a: int = 1
  b: int = 2
print(get_annotations(C))


def f(x: Cls) -> None: ...
class Cls: pass

# 3.14 の遅延評価では:
# {'x': <class 'Cls'>, 'return': None}
print(get_annotations(f))

参考資料

マルチインタープリタ

PythonではインタープリタがPythonのスクリプトファイルをコンパイルし、バイトコードを実行しています。
PythonではGlobal Interpreter Lock(GIL)が存在しており、同じインタープリタではバイトコードを実行するスレッドが1つに制限されます。この機構は安全にPythonを動かす反面、並行性(Concurrency)は実現できるが並列性(Parallelism)を実現できない障害となっています。

Python3.14ではインタープリタを複数動かすことができるようになりました。すなわちGILが並列性(Parallelism)の実行の障害になりません。

以下のコードは一つのインタープリタをマルチスレッドで動かすケースと、複数のインタープリタで動かすケースの違いを表す例です。

実験コード

サンプルコード

from concurrent.futures import InterpreterPoolExecutor, ThreadPoolExecutor
import os, threading, sysconfig, sys, math, time, builtins

def work(limit: int):
    start = time.perf_counter()
    print(f"start... count_primes limit:{limit} pid:{os.getpid()} thread_id:{threading.get_ident()} id(main): {id(sys.modules['__main__'])} id(builtins): {id(builtins)}", flush=True)
    cnt = 0
    for n in range(2, limit):
        root = int(math.isqrt(n))
        for d in range(2, root + 1):
            if n % d == 0:
                break
        else:
            cnt += 1
    print('end... count_primes', limit, time.perf_counter() - start, flush=True)
    return cnt

if __name__ == "__main__":
    # GILが有効を確認s
    print("free-threaded build?  ", bool(sysconfig.get_config_var("Py_GIL_DISABLED")))
    print("GIL enabled at runtime:", getattr(sys, "_is_gil_enabled", lambda: True)())
    pattern = [1_000_000, 500_000, 900_000]
    print(f"id(main): {id(sys.modules['__main__'])} id(builtins): {id(builtins)}")
    print('****** 直列')
    start = time.perf_counter()
    results = [work(p) for p in pattern]
    print(time.perf_counter() - start, results)

    print('****** ThreadPoolExecutor')
    start = time.perf_counter()
    with ThreadPoolExecutor() as pool:
        results = list(pool.map(work, pattern))
    print(time.perf_counter() - start, results)

    print('****** InterpreterPoolExecutor')
    start = time.perf_counter()
    with InterpreterPoolExecutor() as pool:
        results = list(pool.map(work, pattern))
    print(time.perf_counter() - start, results)

実行結果

free-threaded build?   False
GIL enabled at runtime: True
id(main): 4539170176 id(builtins): 4538709984
****** 直列
start... count_primes limit:1000000 pid:17971 thread_id:140704398269376 id(main): 4539170176 id(builtins): 4538709984
end... count_primes 1000000 4.183276683965232
start... count_primes limit:500000 pid:17971 thread_id:140704398269376 id(main): 4539170176 id(builtins): 4538709984
end... count_primes 500000 1.098561302991584
start... count_primes limit:900000 pid:17971 thread_id:140704398269376 id(main): 4539170176 id(builtins): 4538709984
end... count_primes 900000 2.609074859006796
7.891064839961473 [78498, 41538, 71274]
****** ThreadPoolExecutor
start... count_primes limit:1000000 pid:17971 thread_id:123145587011584 id(main): 4539170176 id(builtins): 4538709984
start... count_primes limit:500000 pid:17971 thread_id:123145603801088 id(main): 4539170176 id(builtins): 4538709984
start... count_primes limit:900000 pid:17971 thread_id:123145620590592 id(main): 4539170176 id(builtins): 4538709984
end... count_primes 500000 3.4312552110059187
end... count_primes 900000 6.604183859017212
end... count_primes 1000000 7.053750675986521
7.054385666968301 [78498, 41538, 71274]
****** InterpreterPoolExecutor
start... count_primes limit:1000000 pid:17971 thread_id:123145620590592 id(main): 4555515168 id(builtins): 4552875904
start... count_primes limit:900000 pid:17971 thread_id:123145587011584 id(main): 4553418016 id(builtins): 4550778752
start... count_primes limit:500000 pid:17971 thread_id:123145603801088 id(main): 4554466592 id(builtins): 4551827328
end... count_primes 500000 1.2162047760211863
end... count_primes 900000 2.9336666130111553
end... count_primes 1000000 3.3933817240176722
3.4818163299933076 [78498, 41538, 71274]

この実験では直列では7.9秒程度、ThreadPoolExecutorでは7秒程度、InterpreterPoolExecutorでは3.5秒程度となっています。
ThreadPoolExecutorはGILが有効なため、同時実行の効果が限られていますが、InterpreterPoolExecutorではその制限がないことがわかります。
また、builtinssys.modules['__main__']が別のIDとなっており、独立している環境であることが観測できます。

現時点ではマルチインタープリタにはいくつかの制限があります。

  • 各インタープリタの起動はまだ最適化されていない
  • 各インタープリタは必要以上にメモリを使用します(インタープリタ間の広範な内部共有の作業は継続中です)
  • インタープリタ間でオブジェクトやその他のデータを実際に共有するためのオプションはまだ多くありません(現時点でmemoryview と Queue に限られる)
  • PyPI 上の多くのサードパーティ拡張モジュールはまだ複数のインタープリタと互換性がありません (すべての標準ライブラリ拡張モジュールは互換性があります)
  • 複数の独立したインタープリタを使用するアプリケーションを書くアプローチは、今のところPythonユーザーにはほとんど馴染みがありません。

参考資料

Template String

f-stringは「{...} を評価して即座に文字列へ埋め込む」仕組みです。
そのため埋め込む“直前”に値をフックして加工・検査する手段がありません。
その結果、SQLやHTMLなどを作る場合に安全に使用できないケースが存在します。

以下のコードはf-stringとt-stringの挙動を比べたものとなります。

from html import escape
from string.templatelib import Template, Interpolation
user_input = '<img src=x onerror=alert(1)>'

print('f-string ----------')
html_f = f"<p>Hello, {user_input}</p>"
print(html_f)
# => <p>Hello, <img src=x onerror=alert(1)></p>   ← 危険(イベントハンドラが生きたまま)

print('t-string------------')
def html(tmpl: Template) -> str:
    """
    t-string から静的部はそのまま、補間値だけ HTML エスケープして連結
    あくまで例なので、プロダクトコードには使用しないこと
    """
    out = []
    for part in tmpl:
        if isinstance(part, Interpolation):
            val = part.value
            print('  ', val, part.expression, part.format_spec)
            # 属性辞書を特別扱い: key="value" に展開
            if isinstance(val, dict):
                attrs = []
                for k, v in val.items():
                    if v is None:
                        continue
                    attrs.append(f'{k}="{escape(str(v), quote=True)}"')
                out.append(" ".join(attrs))
            else:
                # 通常の補間は HTML エスケープ
                out.append(escape(str(val), quote=True))
        else:
            # 静的テキストはそのまま
            out.append(part)
    return "".join(out)

safe = html(t"<p>Hello, {user_input}</p>")
print(safe)
# => <p>Hello, &lt;img src=x onerror=alert(1)&gt;</p>  ← 無害化(テキスト化される)

参考資料

括弧なしの except および except* 式を許可する

python3.13以前では以下の書き方は許容されていませんでしたが、3.14で許容されるようになりました。

try:
    pass
except ValueError, ZeroDivisionError:
    print('Error!')

参考資料

finally区のフローの制限

finally区にreturn, break, continueを実行すると警告が出力されるようになりました。
たとえば、以下のようにfinally区でreturnを行った場合、その例外やreturnは握りつぶされます。

def f():
    try:
        1 / 0
        return "...1"
    except:
        return "...2"
    finally:
        return "...3"      # ← ここで例外が再送出されない

print(f()) # ...3となる

この挙動を警告する意図で、finally区でreturn, break, continue区があると警告が発生するようになりました。

参照

zstdのサポート

zstdによる圧縮、解凍がサポートされました。

from compression import zstd
import math
# zstdで圧縮解凍
# https://ja.wikipedia.org/wiki/Zstandard
data = str(math.pi).encode() * 20
print("元データ:", data)
compressed = zstd.compress(data)
print(compressed)
ratio = len(compressed) / len(data)
print(f"Achieved compression ratio of {ratio} 圧縮サイズ:{len(compressed)}")

# 伸張
restored = zstd.decompress(compressed)
print("復元データ:", restored)
assert restored == data

参考

CPython 用の安全な外部デバッガインターフェース

sys.remote_execを使用して、起動中のプロセス上で指定のスクリプトを実行することが可能になりました。
これにより起動中のプロセスに対して、プロセスの再起動なしにデバッグすることが可能になります。
ただし、権限の問題があるので、必要に応じてsudoを実行してください。

remote_execの使用例

リモート実行の例

フォルダ構成

├── controller.py
├── remote_script.py
└── target.py

target.py
スクリプトを実行する対象となるプロセスで動かすスクリプト

# target.py -- a long-running Python 3.14 process to be remote-exec'ed.
import os, time, pathlib, sys

pid = os.getpid()
pathlib.Path("/tmp/target.pid").write_text(str(pid))
print(f"TARGET PID: {pid}", flush=True)

# Keep the interpreter running to provide safe execution points.
try:
    while True:
        time.sleep(0.5)
except KeyboardInterrupt:
    print("Exiting target...")

remote_script.py
リモートで実行するスクリプト

# remote_script.py -- this file is executed *inside* the target process via sys.remote_exec()
import os, pathlib, sys, time

# Simple observable side-effect
p = pathlib.Path("/tmp/remote_exec_ok")
p.write_text(f"remote code ran in PID {os.getpid()}\n")

# Also print to the target's stdout (visible in `docker compose logs -f app`)
print(f"[remote] Hello from remote execution in PID {os.getpid()}!", flush=True)

controller.py
remote_script.pyをtarget.pyの動くプロセスで実行させる

# controller.py -- call sys.remote_exec() from inside the same container.
# Usage:
#   python controller.py [/abs/or/relative/path/to/script.py]
# If script path is omitted, defaults to /work/remote_script.py

import os, sys, pathlib, time

def main():
    import sys as _sys
    script = pathlib.Path(sys.argv[2])
    pid = int(sys.argv[1])
    if not script.exists():
        print(f"ERROR: Script not found: {script}", file=sys.stderr); sys.exit(2)
    print(f"Submitting remote_exec(pid={pid}, script={script}) ...", flush=True)
    try:
        # https://docs.python.org/ja/3/library/sys.html#sys.remote_exec
        _sys.remote_exec(pid, str(script))
        print("Submitted. Code will execute at the next safe execution point.")
    except Exception as e:
        print(f"remote_exec failed: {e}", file=sys.stderr); sys.exit(3)

if __name__ == "__main__":
    main()

使い方:

  1. target.pyの起動
python target.py
  1. 新しいターミナルをひらいて、リモートスクリプトを実行
python  40284 ./remote_script.py
  • 40284 は 1)のPID。

参考

Asyncioイントロスペクション機能

asyncio用のツールとしてpsコマンドとpstreeコマンドが追加されました。

# 当該プロセスで動作する非同期タスクの一覧が表示される
sudo python  -m asyncio ps 非同期タスクが動作するプロセスID
# 非同期タスクのコールツリーを可視化
sudo python  -m asyncio pstree 非同期タスクが動作するプロセスID

詳しい使用例については下記を参照してください。

その他

脚注
  1. Classやfunctionにたいしての__annotations__は3.14でもエラーにはなりません。 ↩︎

Discussion