CLOVER🍀

That was when it all began.

Pythonで型ヒント(Type Hints)を試してみる(+Mypy)

これは、なにをしたくて書いたもの?

簡単なPythonスクリプトを書く時はEmacsでlsp-modeを使っていることが多いのですが、型定義を補完してくるのがよく目に入るので
せっかくなので少し押さえておこうかなと思いまして。

Pythonの型ヒント

Pythonの型ヒントに関するドキュメントはこちら。

typing --- 型ヒントのサポート — Python 3.10.13 ドキュメント

参考)

Python最新バージョン対応!より良い型ヒントの書き方 | gihyo.jp

詳細な仕様はPEP 484、

PEP 484 – Type Hints | peps.python.org

導入に関する説明はPEP 483にあります。

PEP 483 – The Theory of Type Hints | peps.python.org

型ヒントを使った例を見てみましょう。

以下はstr型のnameを受け取り、str型の戻り値を返す関数です。

def greeting(name: str) -> str:
    return 'Hello ' + name

こんな感じで変数の型は:の後ろに書き、関数の戻り値の型は->の後ろに書くようです。

いくつか特徴的なものを見てみましょう。

指定する型は、基本的には組み込み型のものですね。

組み込み型 — Python 3.10.13 ドキュメント

また、typingモジュールを使うことで型定義に使える内容が増えます。一部を載せておきましょう。

  • 特殊型付けプリミティブ
    • 特殊型
      • typing.Any
      • typing.NoReturn
      • typing.TypeAlias
    • 特殊形式
      • typing.Tuple
      • typing.Union
      • typing.Optional
      • typing.Callable
        • collections.abc.Callableを使うこと
      • typing.Concatenate
      • class typing.Type
      • typing.Literal
      • typing.ClassVar
      • typing.Final
      • typing.Annotated
      • typing.TypeGuard
    • Building generic types
      • ...
    • Other special directives
      • ...
  • Generic concrete collections
    • ...
  • 抽象基底クラス
    • ...

挙げていくとキリがないので、途中から端折りました…。

ちなみに、Pythonでは型ヒントを必須にするつもりはなく、動的型付け言語であることは変わらないそうです。

It should also be emphasized that Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.

PEP 484 – Type Hints / Rationale and Goals / Non-goals

よって、型ヒントは実行時には機能せず、誤った型を指定してもふつうに実行できます。

bad_type.py

def add(a: int, b: int) -> int:
    return a + b

print(add("foo", "bar"))
$ python3 bad_type.py
foobar

とはいえ、lsp-modeやPyCharmといった型ヒントを利用できる環境では、誤った型を指定すると警告が表示されます。

Mypy

MypyはPythonの型チェックツールです。

mypy - Optional Static Typing for Python

GitHubリポジトリーはこちら。

GitHub - python/mypy: Optional static typing for Python

例はこちらに記載があり、typingもここでは使われています。

mypy - Examples

ドキュメントはこちら。

mypy 1.10.1 documentation

Getting started。

Getting started - mypy 1.10.1 documentation

型の指定方法はこちらのチートシートを見るのがよいでしょうね。

Type hints cheat sheet - mypy 1.10.1 documentation

使い方は、チェック対象のファイルやディレクトリを指定して実行するようです。

$ mypy foo.py bar.py some_directory

The mypy command line - mypy 1.10.1 documentation

設定ファイルも定義できるみたいですね。

The mypy configuration file - mypy 1.10.1 documentation

Pythonスクリプト内にインラインでそのファイルに対する設定を書くこともできるようです…。

Inline configuration - mypy 1.10.1 documentation

今回はMypyは少し試してみるくらいにしましょう。

環境

今回の環境はこちら。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

Pythonの型ヒントを試してみる

それでは、型ヒントを試してみましょう。

ざっと例を。

ype_hint_getting_started.py

## str型
message: str = "Hello Python!!"

## bool型
contains_python: bool

if "Python" in message:
    contains_python = True
else:
    contains_python = False

## list[str]
programming_languages: list[str] = ["Python", "Java", "JavaScript", "TypeScript", "C#"]

for language in programming_languages:
    print(language)

## 関数定義
def plus(a: int, b: int) -> int:
    return a + b

## クラス定義
class Person:
    first_name: str
    last_name: str
    age: int

    def __init__(self, first_name: str, last_name: str, age: int) -> None:
        self.first_name = first_name
        self.last_name = last_name
        self.age = age

    def say(self) -> str:
        return f"名前は{self.last_name}{self.first_name}です"

katsuo = Person("カツオ", "磯野", 11)
print(katsuo.say())

これで終わってもなんなので、いくつか気になることを書いていきましょう。

辞書型は?

dictに2つ型引数を指定します。

people: dict[str, int] = {
    "磯野カツオ": 11,
    "磯野ワカメ": 9,
    "フグ田タラオ": 3
}
for文の中に登場する変数の型の指定は?

先ほどのサンプルでこういうものを書きましたが、language変数の型がありません。

## list[str]
programming_languages: list[str] = ["Python", "Java", "JavaScript", "TypeScript", "C#"]

for language in programming_languages:
    print(language)

このlanguageの型を指定するには?とちょっと思ったりするのですが、これは指定しなくて良さそうです。ループに使用している
コレクションの型定義から決まっているようです。

この記述でlsp-modeなどでもlanguageはstr型であることを認識していますし、後述のMypyでの型チェックでもNGになりません。

戻り値のない関数の戻り値の型は?

Noneです。

def hello() -> None:
    print("Hello World")
コンストラクタの戻り値の型は?

Noneです。

    def __init__(self, first_name: str, last_name: str, age: int) -> None:

クラスメソッドでそのクラスのインスタンスを返す場合の戻り値の型は?

こんな感じで書きたくなるのですが

class Person:
    first_name: str
    last_name: str
    age: int

    def __init__(self, first_name: str, last_name: str, age: int) -> None:
        self.first_name = first_name
        self.last_name = last_name
        self.age = age

    @classmethod
    def create(cls, first_name: str, last_name: str, age: int) -> Person:
        return cls(first_name, last_name, age)

これは実行できなかったりします。

Traceback (most recent call last):
  File "/path/to/some_file.py", line 6, in <module>
    class Person:
  File "//path/to/some_file.py", line 17, in Person
    def create(cls, first_name: str, last_name: str, age: int) -> Person:
NameError: name 'Person' is not defined

文字列にする必要があるみたいです。

    @classmethod
    def create(cls, first_name: str, last_name: str, age: int) -> "Person":
        return cls(first_name, last_name, age)

型ヒントのドキュメントでは、typing.TypeVarを使って戻り値で使う型を定義しています。

typing --- 型ヒントのサポート / モジュールの内容 / Building generic types / class typing.TypeVar

関数を受け取る関数を定義するには?

collections.abc.Callableを使います。

from collections.abc import Callable

## 引数ありの関数を受け取る
def filter(languages: list[str], func: Callable[[str], bool]) -> list[str]:
    return [language for language in languages if func(language)]

## 引数なしの関数を受け取る
def hello(name_generator: Callable[[], str]) -> None:
    print(f"Hello {name_generator()}")

print(filter(["Java", "JavaScript", "Python"], lambda l: "JavaScript" in l))
hello(lambda: "Callable" )

Mypyを使ってみる

最後にMypyを使ってみましょう。

インストール。

$ pip3 install mypy

インストールされたライブラリーの一覧。

$ pip3 list
Package           Version
----------------- -------
mypy              1.10.1
mypy-extensions   1.0.0
pip               22.0.2
setuptools        59.6.0
tomli             2.0.1
typing_extensions 4.12.2

最初に書いた誤った型を指定して関数を呼び出しているスクリプトで試してみましょう。

bad_type.py

def add(a: int, b: int) -> int:
    return a + b

print(add("foo", "bar"))

こんな感じで怒られます。

$ mypy bad_type.py
bad_type.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"  [arg-type]
bad_type.py:4: error: Argument 2 to "add" has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

カレントディレクトリ配下や特定のディレクトリ配下のスクリプトをまとめて指定したい場合は、以下のようにディレクトリを指定すれば
OKです。

$ mypy .
$ mypy [directory]

また--disallow-untyped-defsオプションを追加することで、型指定のない定義があるとエラーにできます。

$ mypy --disallow-untyped-defs bad_type.py

たとえばこういうコードを追加して実行すると

def message(s):
    print(s)

型アノテーションがないと怒られます。

bad_type.py:6: error: Function is missing a type annotation  [no-untyped-def]

こんなところでしょうか。

おわりに

Pythonで型ヒントを試してみました。合わせて、Mypyも使ってみました。

ちょっとした導入的なものでしたが、今まで完全に雰囲気で型ヒントを見ていたのでこの機会にある程度向き合っておいてよかったかなと。

Pythonで型ヒントを使うかどうかは微妙なところな気がしますが、個人的にはここで書くスクリプトについては適用していきたいなと
思ったりしています。