💨

Pythonの型ヒントについて初心者が知るべきことを解説する

に公開

はじめに

初投稿にしては渋い内容だが、Pythonの型ヒントについて解説したい。

例えば、あなたが新たにプロジェクトにアサインされたとて、この関数を見て、すぐに正しく使えるだろうか?

def authenticate_user(credentials, token):

・credentialsって何?ユーザー名とパスワードの辞書?特定のクラスのオブジェクト
・tokenって文字列?それとも何かのオブジェクト?
・戻り値は何が返ってくるの?True/False?ユーザー情報?
こういった疑問が湧いてくるはずだ。

Pythonは動的型付け言語であり、コードが大規模になるにつれて「この変数には何が入るのか?」「この関数は何を返すのか?」といった疑問が生じてくる。

そんな課題を解決するために導入されたのが型ヒントだ。Python 3.5から登場し、バージョンアップごとに機能が拡充されてきた機能だ。

以前はPythonで型ヒントをつけることはなかったが、書籍「ロバストPython」の影響なのか、最近のPythonプロジェクトでは、型ヒントの使用するところも増えてきている。

型ヒントとは何か?

Pythonの動的型付けの特徴

通常のPythonコードでは、変数の型が実行時に決定される。

value = 10      # int型
value = "hello" # str型に再代入可能

この柔軟さは便利だが、コードの意図が不明確になりがちだ。
型ヒントをつけると、以下のようにIDE上で赤色のメッセージが表示され、同じ変数には同じ型しか入れられないように開発者に警告を与えることができる。

型ヒント導入のメリット

型ヒントを使用することで、以下のようなメリットが得られる:

  • コードの可読性向上: 変数や関数の型が明確になり、意図が伝わりやすくなる
  • バグの早期発見: 静的解析ツールと連携して、実行前に型エラーを検出できる
  • IDEサポートの強化: より正確なコード補完やリアルタイムの型チェックが可能
  • チーム開発の効率化: コードレビューの負担が軽減され、コミュニケーションが円滑になる

基本的な型ヒントの書き方

変数への型ヒント

変数名の後にコロン(:)と型名を記述する。

name: str = "Alice"        # 文字列型
age: int = 30              # 整数型
price: float = 99.99       # 浮動小数点数型
is_active: bool = True     # 真偽値型

*) 変数の型ヒントは冗長であるため、よほどわかりにくい場合を除いてあまり推奨されていない。

関数への型ヒント

引数と戻り値に型情報を付与できる。書籍「ロバストPython」の中では関数やクラスメソッドへの型ヒントは特に推奨されている。
パッケージなどで分けて外部から呼び出す時、特にわかりやすくなるからだ。

def calculate_tax(price: int, rate: float) -> int:
    return int(price * (1 + rate))

result = calculate_tax(1000, 0.1)  # 1100

コレクション型の型ヒント

コレクション型にもヒントを設定できる。
以下のように何のリスト、セット、辞書なのか。タプルなら中身まで詳細な設定が可能だ。

リスト、セット、タプル

# リスト
names: list[str] = ["Alice", "Bob", "Charlie"]
scores: list[int] = [85, 92, 78, 90]

# セット
unique_names: set[str] = {"Alice", "Bob", "Charlie"}

# タプル(固定長)
person: tuple[str, int, bool] = ("Alice", 25, True)

# タプル(可変長)
rgb_colors: tuple[int, ...] = (255, 128, 64, 32)

辞書型

# 基本的な辞書
ages: dict[str, int] = {"Alice": 25, "Bob": 30}

# 複雑な構造の辞書
hobbies: dict[str, list[str]] = {
    "Alice": ["reading", "swimming"],
    "Bob": ["gaming", "cooking"]
}

typingモジュールを活用した高度な型ヒント

typingモジュールを使用したらさらに複雑な型ヒントが可能となる。

Optional - Null許可型

これは、値がNoneになる可能性がある場合に使用する。
ただし、最近のPythonバージョンだとは型 | Noneという書き方が主流だ。

from typing import Optional

def find_user_by_id(users: list[dict[str, str]], user_id: str) -> dict[str, str] | None:
# Optional[dict[str, str]]でも可能
    for user in users:
        if user.get('id') == user_id:
            return user
    return None

users = [
    {'id': '001', 'name': '田中'},
    {'id': '002', 'name': '佐藤'},
]

user = find_user_by_id(users, '001') # userはdict[str, str]かNoneが入る
if user is not None:
    print(user['name'])  # 田中

Union - 複数型の許可

複数の型のうちいずれかを取る場合に使用する。これも最近は|(パイプ)で代用できる。

from typing import Union
# Union[int, str]でも可能だ
def get_user_data(user_id: int | str) -> dict[str, str] | None:
    mock_data = {
        1: {'name': '田中太郎', 'role': 'admin'},
        'user_002': {'name': '佐藤花子', 'role': 'user'}
    }
    return mock_data.get(user_id)

# 整数でも文字列でもアクセス可能
user1 = get_user_data(1)
user2 = get_user_data('user_002')

Callable - 関数型指定

関数を引数として受け取る高階関数で使用する。
以下の例だと、任意の方Tを一つの引数に戻り値にstrとした関数をprocessorの型として設定している。

from typing import Callable, TypeVar

# TypeVarは任意の型を表す。
T = TypeVar('T')

def process_items(
    items: list[T],
    processor: Callable[[T], str] # 型がTで戻り値がstrだ。
) -> list[str]:
    return [processor(item) for item in items]

def format_user(user: dict[str, str]) -> str:
    return f"{user['name']} ({user['email']})"

user_list = [
    {'name': '田中太郎', 'email': 'tanaka@mail.com'},
    {'name': '佐藤花子', 'email': 'sato@mail.com'},
]

formatted = process_items(user_list, format_user)
print(formatted)  # ['田中太郎 (tanaka@mail.com)', '佐藤花子 (sato@mail.com)']

Final - 定数宣言

変更されるべきでない定数を宣言する。以下の例だとAPI_VERSIONを変更しようとするとエラーになる。

from typing import Final

API_VERSION: Final[str] = 'v2.1'
MAX_CONNECTIONS: Final[int] = 100

# API_VERSION = 'v2.2'  # 型チェッカーでエラーになる

Self - 自分自身の型

クラスメソッドで、自分自身のインスタンスを返す場合に使用する。メソッドチェーンでよく用いられる。

from typing import Self

class QueryBuilder:
    def __init__(self) -> None:
        self._table = ''
        self._conditions = []
    
    def from_table(self, table_name: str) -> Self:
        self._table = table_name
        return self
    
    def where(self, condition: str) -> Self:
        self._conditions.append(condition)
        return self
    
    def build(self) -> str:
        query = f'SELECT * FROM {self._table}'
        if self._conditions:
            query += f" WHERE { ' AND '.join(self._conditions)}"
        return query

query = QueryBuilder().from_table('user').where('age > 20').where('active = true').build()
print(query)  # SELECT * FROM user WHERE age > 20 AND active = true

Generic - ジェネリック型

型パラメータを持つクラスを定義できる。この場合、インスタンス作成時にどの型を使用するのかが決定される。

from typing import Generic, TypeVar

T = TypeVar('T')

class Cache(Generic[T]): # この時点ではTは任意の型
    def __init__(self):
        self._data: dict[str, T] = {}
        
    def set(self, key: str, value: T):
        self._data[key] = value
    
    def get(self, key: str) -> T | None:
        return self._data.get(key)
    
    def get_all(self) -> list[T]:
        return list(self._data.values())

# ここで型を指定する。
string_cache: Cache[str] = Cache() # Tはstr
string_cache.set('greeting', 'こんにちは')
print(string_cache.get('greeting'))  # こんにちは

dict_cache: Cache[dict[str, int]] = Cache() # Tはdict[str, int]
dict_cache.set('scores', {'math': 90, 'english': 80})
print(dict_cache.get_all())  # [{'math': 90, 'english': 80}]

Literal - リテラル値制限

変数が取りうる値を特定の値に制限する。複数の候補の中から値を指定するのだ。

from typing import Literal

def get_user_permission_level(
    role: Literal['admin', 'user', 'guest']
) -> int:
    permissions_map = {
        'admin': 10, 'user': 5, 'guest': 1
    }
    return permissions_map[role]

level = get_user_permission_level('admin')  # 10
# level = get_user_permission_level('invalid')  # 型チェッカーエラー

TypedDict - 辞書構造定義

辞書のキーと各値の型を厳密に定義する。NotRequiredをつけたキーは、設定してもしなくてもよいキーだ。これは辞書の中身を固定したい場合に非常に重宝する。

from typing import Literal, NotRequired, TypedDict

class UserProfile(TypedDict):
    id: int
    name: str
    role: Literal['admin', 'user', 'guest']
    last_login: NotRequired[str]  # 任意フィールド
    
def create_user_profile(
    user_id: int,
    name: str,
    role: Literal['admin', 'user', 'guest']
) -> UserProfile:
    return {'id': user_id, 'name': name, 'role': role}

user = create_user_profile(1, '田中太郎', 'admin')
print(user)  # {'id': 1, 'name': '田中太郎', 'role': 'admin'}

まとめ

型ヒントは、Pythonコードの品質と保守性を大幅に向上させる強力な機能だ。

覚えておきたいポイント

  • 基本的な型(int、str、float、bool)から始める
  • コレクション型(list、dict、tuple)の型指定を覚える
  • Optional、Union、Literalで柔軟な型制約を設定する
  • Generic、TypedDictで複雑な構造にも対応する
  • IDEや静的解析ツール(mypyなど)と組み合わせて効果を最大化する

型ヒントは実行時の動作には影響しないが、開発体験を大きく改善する。特に大規模なコードを書くことになると、その効果をより実感できる。

以上、今回は型ヒントについて解説した。
弊社では、Pythonをはじめとしてエンジニア向けの講座をたくさん用意している。各講座10時間を超えるような長い物でその分野のことをみっちりと学べるようになっている。
ご活用していただけると幸いだ。

コース一覧

Discussion