Unyablog.

のにれんのブログ

# type: ignore じゃなくて typing.cast を使おう

Python で型アノテーションを使うとき、なんだかんだ # type: ignore したいときがあると思う。フィールドの全部が optional だけど、実際にはこのコードパスなら絶対 optional じゃない、とか。 API まわり触ってると割とよくある。

if foo is None とか assert とか TypeGuard 的なものでチェックしてもいいんだけど、アクセス前にいちいち assert するのめんどくさいし、コードはごちゃつくし、なにより無駄な処理だなって気持ちになる。Python でやりたいのは雑プログラミングであって堅牢なのが書きたければ Optional 対応がちゃんとしてるものを使えばいいんだよな

# type: ignore はよくない

そういうとき、 # type: ignore をつけると無視してエディタの警告を消し去ることができる。

ただ、 # type: ignore はあまり考えずに書くと型チェック除外の影響範囲が想定以上に大きくなってしまうことがある(以下 Pylance で確認)。

@dataclass
class Foo:
    a: float | None

def call_function(a: float):
    pass

foo = Foo(1)
call_function(foo.a) # type: ignore

たとえば上記のように書くと、 call_function 関数の呼び出し全体のチェックが行われなくなる。すなわち、ここで引数の数が足りなかったり多すぎたりしてもチェックすることができない。

ちゃんとチェックするには下のように書く必要がある。

call_function(
    foo.a, # type: ignore
    None, # => error!
)

これだけなら、一行で書かないように気をつければいいじゃんって程度だが、落とし穴はまだ深い。

例えば、以下のようなケースでも普通にエラーなく通ってしまう。

call_function(
    1,
    foo.a, # type: ignore
)

以下は第一引数の形が明らかに異なるが、警告なく通ってしまう。なんで?

call_function(
    "string",
    foo.a, # type: ignore
)

とにかく #type: ignore はできるだけ避けたほうがいい。

typing.cast

ここで typing.cast を使う。

typing.cast は型と値を引数に持って、値はそのまま返しつつ型情報のみ引数のものに変化する。

from typing import cast

call_function(cast(float, foo.a))

このようにすると、 call_function に渡している型は float ということになるので、# type: ignore を避けつつエディタの警告を消すことができるし、型の安全性を無視しているという意図もコードに含めることができる。

便利!