Python を 3.14 に上げたら、触っていないコードが次のエラーで落ちます。
TypeError: send() takes 2 positional arguments but 3 were given
コピペ検索用の表記ゆれです。包んでいる関数によって文言が変わります(すべて実測)。
TypeError: send() takes 2 positional arguments but 3 were givenTypeError: handle() takes 1 positional argument but 2 were givenTypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int'TypeError: 'Api' object is not iterable(dictなどの組み込みを包んだ場合)
エラー文に functools も partial も出てきません。 落ちているのは functools.partial で作った callable です。
何が起きているか
send() は 2 引数の関数で、呼び出し側も引数を 2 つしか渡していません。それなのに「3 つ渡された」と言われます。
import functools
def send(channel, message):
return f"{channel}:{message}"
class Notifier:
notify = functools.partial(send, "ops") # channel を "ops" に固定
Notifier().notify("hello") # Python 3.13 → "ops:hello" / 3.14 → TypeError
増えている 1 つは、インスタンスです。 Python 3.14 で functools.partial がメソッド記述子(method descriptor)になりました。 クラス属性としてインスタンス経由で読み出すと、通常のメソッドと同じように第 1 引数へ self が差し込まれます。
つまり実際に呼ばれているのはこうです。
send("ops", Notifier_instance, "hello") # 引数が 3 つ
pow を包んでいた場合に 'int', 'Handler', 'int' という並びが出るのも同じ理由です。pow(2, インスタンス, 3) になっています。
直し方
staticmethod() で包みます。
class Notifier:
notify = staticmethod(functools.partial(send, "ops"))
staticmethod はインスタンスを束縛しない記述子なので、partial が記述子として振る舞う余地がなくなります。
この書き方は 3.12・3.13・3.14 のどれでも同じ結果になります(実測)。前にも後ろにも安全なので、まだ 3.14 に上げていないコードへ先に入れておけます。
逆に、self を渡したいのであれば functools.partialmethod を使います。
class Notifier:
def _send(self, message):
return f"{self.channel}:{message}"
notify = functools.partialmethod(_send)
これは 3.12 から 3.14 まで同じように動きます(実測)。partial が記述子になった経緯は、What’s New in Python 3.14 の functools の項に説明があります。
落ちるのは、クラス属性の partial をインスタンス経由で呼んだときだけ
partial を使っているコードが全部落ちるわけではありません。落ちるのは、次の条件がすべて重なったときだけです。
functools.partialである- クラス属性に置いている
- dict などに入れず、直接その属性にしている
- インスタンス経由(
obj.attr(...))で呼んでいる
Python 3.14 で、次の 5 つを実際に走らせた結果です。
| 書き方 | 3.14 |
|---|---|
class H: calc = partial(pow, 2) を H().calc(3) で呼ぶ | TypeError |
同じものを H.calc(3)(クラス経由)で呼ぶ | 通る |
class H: HANDLERS = {"p": partial(pow, 2)} を H().HANDLERS["p"](3) | 通る |
calc = partial(pow, 2)(モジュール直下) | 通る |
def __init__(self): self.calc = partial(pow, 2) | 通る |
クラス経由(H.calc(3))が通るのは、記述子がインスタンスを束縛するのがインスタンス経由のときだけだからです。
dict に入れた partial が通るのも同じ理由で、属性として読み出されていません。ハンドラ表を {"create": partial(...), "delete": partial(...)} の形で持っているコードは、それがクラス属性であっても影響を受けません。
3.13 の予告と、その拾い方
このエラーには予告があります。Python 3.13 で同じコードを走らせると、次が出ます。
FutureWarning: functools.partial will be a method descriptor in future Python
versions; wrap it in staticmethod() if you want to preserve the old behavior
この警告は修正方法(staticmethod() で包む)まで書いています。そして -W を付けなくても表示されます。 DeprecationWarning は既定のフィルタでほぼ隠れますが、これは FutureWarning で、既定で表示される側です(実測)。
警告が出るのは 3.13 だけです。 3.12 では -W always を付けても何も出ません(実測)。3.12 から 3.14 へ直接上げると、予告を 1 度も見ないまま TypeError に当たります。
ただし、この警告はクラス属性を定義した時点では出ません。インスタンス経由でアクセスした瞬間に出ます(実測:class H: calc = partial(pow, 2) を定義しただけでは警告ゼロ、H().calc(3) を実行した行で初めて出る)。つまり 3.13 で拾えるのは、その実行で実際に通ったアクセス点だけです。テストが踏まなかった経路の partial は現れません。
上げる前に洗い出すなら、-W error::FutureWarning を付けて例外にしたうえで、対象のコードパスをテストや CI で網羅してください。「3.13 で動かして警告が出なかった」は「そのとき通った範囲には無かった」であって、「存在しない」ではありません。
切り分け
- エラー文に自分のクラス名が引数として出てくる → このエラーです。
'int', 'Handler', 'int'のHandlerや、'Api' object is not iterableのApiは、差し込まれたインスタンスです。 - 引数の数が、渡した数よりちょうど 1 つ多い → 同じく。増えた 1 つがインスタンスです。
partialを使っているのに落ちない → 上の表のどれかに当たっています。クラス属性に直接置いていて、かつインスタンス経由で呼んでいるものだけが落ちます。TypeError: 'X' object is not iterableが出た →dictやsetなど、位置引数の意味が違う組み込みを包んでいます。原因は同じ(インスタンスが第 1 引数に入った)ですが、エラーの形だけが変わります。- 3.14 に上げていないのに同じエラーが出る → 別の原因です。3.13 以前の
partialはインスタンスを束縛しません(実測)。 partialではなくlambdaや入れ子関数で似た症状 → 別の記事です。3.14 はmultiprocessingの既定 start method も変えており、そちらは pickle の失敗として出ます。Can’t pickle <function <lambda>> の直し方 を参照してください。
検証環境
python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1(Python 3.14.6)、ネットワーク不要- 再現:
class Handler: calculate = functools.partial(pow, 2)をHandler().calculate(3)で呼び、TypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int'が出て終了コード 1 - 修正:
staticmethod(functools.partial(pow, 2))に変え、同じ Python 3.14.6 で終了コード 0・シグネチャ消滅 - 上の表の 5 形、
staticmethodとpartialmethodの前方後方互換、3.13 のFutureWarningが既定で見えること、その警告がクラス属性の定義時ではなくインスタンス経由アクセス時に出ること、3.12 では-W alwaysでも出ないことは、python:3.12-slim(3.12.13)・python:3.13-slim(3.13.14)・3.14.6 の 3 版でそれぞれ実行して確認しました - 3.12 と 3.13 の間、3.13 と 3.14 の間のパッチ版は測っていません