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で型ヒントを䜿うかどうかは埮劙なずころな気がしたすが、個人的にはここで曞くスクリプトに぀いおは適甚しおいきたいなず
思ったりしおいたす。