Error [TypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int']

TypeError: takes 2 positional arguments but 3 were given の直し方(Python 3.14 でクラス属性の functools.partial が壊れる)

FIX SUMMARY verified
Applies when
python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1A functools.partial stored as a class attribute and called through an instance; Python 3.14 makes partial a method descriptor, so the instance is injected as the first positional argument

Verified: reproduced in python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1, then the TypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int' signature was gone after the fix (exit 0).

Python を 3.14 に上げたら、触っていないコードが次のエラーで落ちます。

TypeError: send() takes 2 positional arguments but 3 were given

コピペ検索用の表記ゆれです。包んでいる関数によって文言が変わります(すべて実測)。

  • TypeError: send() takes 2 positional arguments but 3 were given
  • TypeError: handle() takes 1 positional argument but 2 were given
  • TypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int'
  • TypeError: 'Api' object is not iterabledict などの組み込みを包んだ場合)

エラー文に functoolspartial も出てきません。 落ちているのは 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 を使っているコードが全部落ちるわけではありません。落ちるのは、次の条件がすべて重なったときだけです。

  1. functools.partial である
  2. クラス属性に置いている
  3. dict などに入れず、直接その属性にしている
  4. インスタンス経由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 iterableApi は、差し込まれたインスタンスです。
  • 引数の数が、渡した数よりちょうど 1 つ多い → 同じく。増えた 1 つがインスタンスです。
  • partial を使っているのに落ちない → 上の表のどれかに当たっています。クラス属性に直接置いていて、かつインスタンス経由で呼んでいるものだけが落ちます。
  • TypeError: 'X' object is not iterable が出たdictset など、位置引数の意味が違う組み込みを包んでいます。原因は同じ(インスタンスが第 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 形、staticmethodpartialmethod の前方後方互換、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 の間のパッチ版は測っていません

検証(machine-verified)

この修正は python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 のバージョン固定コンテナ内で再現し、修正後に TypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int' のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs python/functools-partial-method-descriptor
● reproduce TypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int' present ✓
● apply fix exit 0
● re-run TypeError: unsupported operand type(s) for ** or pow(): 'int', 'Handler', 'int' gone ✓
PASS verified · python@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。