Pythonの型判定で isinstance() を使ったら、bool型なのにint型と判定されてハマった話

Pythonの型判定で isinstance() を使ったら、「bool型」が「int型」と判定されました。なぜこうなったのか、どのようにすればよかったのかをまとめてみました。
2020.02.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

API GatewayとLambdaでWebAPIを作ったとき、リクエストJSONのバリデーションを行うと思います。 その方法はいくつかあります。

  • API Gatewayでやる
  • Lambdaコードでやる(自前 or ライブラリ)

たとえば、Pythonであれば、下記のライブラリがあります。

そんなJSONバリデーションですが、Pythonでisinstance()を使ったときに、bool型なのにint型と判定されるケースに遭遇したので、起こったことやどうすればよかったのかをまとめてみました。

ハマったこと

たとえば

次のようなJSONがあります。TODOリストをイメージしています。

{
    "taskId": "test001",
    "title": "あああ",
    "message": "てすとてすと",
    "deadlineTimestamp": 123456789,
    "isCompleted": False
}

このJSONをisinstance()を使って次のようにバリデーションしていました。(本当は文字数とかも確認してますがここでは省略しています)

def validate1(data):
    task_id = data['taskId']
    title = data['title']
    message = data['message']
    deadline_timestamp = data['deadlineTimestamp']
    is_completed = data['isCompleted']

    if not isinstance(task_id, str):
        return False
    if not isinstance(title, str):
        return False
    if not isinstance(message, str):
        return False
    if not isinstance(deadline_timestamp, int):
        return False
    if not isinstance(is_completed, bool):
        return False
    return True

何が起こったか

上記メソッドのテストを書いているときに気づいたのです。次のJSONがバリデーションOKになることを。

{
    "taskId": "test001",
    "title": "あああ",
    "message": "てすとてすと",
    "deadlineTimestamp": True,
    "isCompleted": False
}

簡単に再現してみる

isinstance()にbool型を与えて、int型か?を確認すると、Trueになりました。

>>> isinstance(True, int)
True

>>> isinstance(False, int)
True

ほかは想定通りになります。

>>> isinstance(True, str)
False

>>> isinstance(123, int)
True

>>> isinstance(123, str)
False

どうしてこうなった

isinstance()の仕様でした。

Return True if the object argument is an instance of the classinfo argument, or of a (direct, indirect or virtual) subclass thereof.

isinstance() — Python ドキュメント

初めて知ったのですが、PythonのBool型は、Integer(整数)のサブクラスとして実装されています。

Python の Bool 型は整数のサブクラスとして実装されています。

Boolean オブジェクト — Python ドキュメント

つまり、PythonのBool型は整数のサブクラスのため、isinstance(True, int)は「int型のサブクラスにBool型があるのでTrueになる」と理解しました。

では、どうするか

type()を使う

オブジェクトの判定であればisinstance()が推奨されていますが、今回のようにJSONバリデーションをする場合はちょっと嬉しくないです。 そこでtype()を使えばと考えました。

引数が1つだけの場合、object の型を返します。

オブジェクトの型の判定には、 isinstance() 組み込み関数を使うことが推奨されます。これはサブクラスを考慮するからです。

type() - Python ドキュメント

実際にやってみた

type()を使えば、型情報が返ってきます。コレを元にif文の条件式を書けばOKです。

>>> type(True)
<class 'bool'>

>>> type(False)
<class 'bool'>

>>> type(123)
<class 'int'>

たとえば

冒頭のバリデーション関数をtype()を使うようにしてみました。

def validate2(data):
    task_id = data['taskId']
    title = data['title']
    message = data['message']
    deadline_timestamp = data['deadlineTimestamp']
    is_completed = data['isCompleted']

    if type(task_id) != str:
        return False
    if type(title) != str:
        return False
    if type(message) != str:
        return False
    if type(deadline_timestamp) != int:
        return False
    if type(is_completed) != bool:
        return False
    return True

さいごに

発見したのは偶然ですが気づけてよかったです。気をつけたいと思いました。

参考