Tasuke HubLearn · Solve · Grow
#Python

型駆動でAIパイプラインを組むためのPython高度テクニック

TypeVarやProtocol、Literal、NewType、dataclass_transformなどを組み合わせ、LLM/ETL/推論ジョブに型駆動の安全性を持たせる方法を解説します。

時計のアイコン23 November, 2025
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

高度な型安全が必要になる場面

  • LLMチェーンが複数モデルをまたぎ、入力/出力の型が動的に変わる。
  • Feature StoreやVector Storeを共通のインターフェースで扱いたい。
  • ルーターパターンで複数のハンドラに処理を振り分ける際、キーと関数の整合性をコンパイル時に確保したい。
  • DataclassやPydanticのラッパーを自作し、型チェッカにカスタム動作を教えたい。

ここでは、標準ライブラリ typing と mypyプラグインAPIを使った高度な型安全テクニックを紹介します。

ベストマッチ

最短で課題解決する一冊

この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。

NewType と LiteralStringでプロンプトインジェクションを抑止

from typing import NewType, LiteralString

SystemPrompt = NewType("SystemPrompt", str)
UserPrompt = NewType("UserPrompt", str)

ALLOWED_TEMPLATE: LiteralString = "You are a helpful assistant"

def build_system_prompt(template: LiteralString) -> SystemPrompt:
    return SystemPrompt(template)

system_prompt = build_system_prompt(ALLOWED_TEMPLATE)
user_prompt = UserPrompt("Summarize the attached report")

# stringをそのまま代入するとmypyエラー
# user_prompt = "free form"  # error: incompatible type

NewTypeでプロンプトの役割を分離し、LiteralStringでテンプレートをコンパイル時に限定できます。外部入力をそのままSystem Promptに流し込むと型エラーになるため、レビュー漏れを防げます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

TypeVarとProtocolでモデルの組合せ爆発を制御

from typing import TypeVar, Protocol, Generic

Doc = TypeVar("Doc", contravariant=True)
Result = TypeVar("Result", covariant=True)

class Stage(Protocol[Doc, Result]):
    def __call__(self, item: Doc) -> Result: ...

class Pipeline(Generic[Doc, Result]):
    def __init__(self, stage: Stage[Doc, Result]):
        self._stage = stage

    def run(self, data: Doc) -> Result:
        return self._stage(data)

class JsonToEmbedding(Stage[str, list[float]]):
    def __call__(self, item: str) -> list[float]:
        obj = json.loads(item)
        return embed(obj["text"])  # 型が保証される

pipe = Pipeline(JsonToEmbedding())
vector = pipe.run('{"text": "hello"}')

Stageに逆変/共変TypeVarを指定し、パイプラインをGenericsで結ぶことで、Stage同士の型整合性をmypyが保証します。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

Router + TypedDict + Literalで安全なハンドラ登録

from typing import Callable, TypedDict, Literal, overload

class ClassificationRequest(TypedDict):
    type: Literal['classification']
    text: str

class EmbeddingRequest(TypedDict):
    type: Literal['embedding']
    text: str

Request = ClassificationRequest | EmbeddingRequest

Handlers = dict[str, Callable[[Request], str | list[float]]]

handlers: Handlers = {}

@overload
def register(type_: Literal['classification']) -> Callable[[Callable[[ClassificationRequest], str]], None]: ...

@overload
def register(type_: Literal['embedding']) -> Callable[[Callable[[EmbeddingRequest], list[float]]], None]: ...

def register(type_):
    def decorator(func):
        handlers[type_] = func
        return func
    return decorator

@register('classification')
def classify(req: ClassificationRequest) -> str:
    return classify_text(req['text'])

LiteralキーとTypedDictハンドラの型を一致させるため、@overloadで登録関数の型を分岐させています。存在しないリクエストタイプを登録しようとするとmypyが検知します。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

dataclass_transformで自作デコレータに型認識させる

from typing import dataclass_transform

@dataclass_transform()
def schema(cls: type[T]) -> type[T]:
    return dataclasses.dataclass(eq=False, frozen=True)(cls)

@schema
class PromptTemplate:
    name: str
    input_vars: tuple[str, ...]

dataclass_transformを付与すると、カスタムデコレータでもmypyがdataclassとして扱い、生成フィールドや補完を理解できるようになります。AI系DSLを自作するときに有用です。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

mypy プラグインでPydantic v2を理解させる

pyproject.tomlpydantic.mypy を有効にし、ConfigDictに応じた型推論をさせます。

[tool.mypy]
plugins = ["pydantic.mypy"]
class QueryModel(BaseModel):
    model_config = ConfigDict(extra='forbid')
    vector: list[float]
    metadata: dict[str, str]

これでextra禁止が型チェックでも反映され、未定義フィールドアクセスをコンパイル時に検知できます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

Typed DFSとExhaustive MatchでLLMステートを管理

from typing import Literal, assert_never

State = Literal['init', 'collect', 'generate', 'done']

def next_state(state: State, event: str) -> State:
    match state:
        case 'init' if event == 'input':
            return 'collect'
        case 'collect' if event == 'ready':
            return 'generate'
        case 'generate' if event == 'finish':
            return 'done'
        case 'done':
            return 'done'
        case other:
            assert_never(other)

assert_neverを使うと、新しい状態を追加した際にmatchの抜け漏れをmypyが指摘してくれるため、LLMステートマシンを安全に保守できます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

ストレージごとのNewTypeとFactoryでミスを防ぐ

VectorId = NewType('VectorId', str)
DocumentId = NewType('DocumentId', str)

class VectorStore(Protocol):
    def upsert(self, vid: VectorId, vector: list[float]) -> None: ...

class DocumentStore(Protocol):
    def save(self, did: DocumentId, text: str) -> None: ...

vector_store.upsert(VectorId('vec-1'), embedding)  # OK
vector_store.upsert(DocumentId('doc-1'), embedding)  # mypy error

IDの混在はLLMパイプラインで頻出するバグです。NewTypeで意味付きIDを作り、Factory関数で生成する習慣を付けます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

Typed Settingsで環境変数を型付け

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    openai_api_key: str
    redis_url: AnyUrl
    concurrency_limit: int = 8

settings = Settings()  # 型安全に参照できる

環境変数をBaseSettings経由にすると、型未満の設定を起動時に検知できます。Literalと組み合わせればフラグ値の制約も可能です。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

テストコードでも型を守る

from typing import Callable

Handler = Callable[[str], str]

@pytest.fixture
def handler() -> Handler:
    return lambda text: text.upper()

@pytest.mark.parametrize('text', ['abc', 'xyz'])
def test_handler(handler: Handler, text: str) -> None:
    result = handler(text)
    assert result.isupper()

テストにも型ヒントを付けると、fixtureのスコープやParametrizeの値がずれた際にmypyが警告を出します。AIプロジェクトはテストコード量が多いため効果的です。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

まとめ

  • NewType, Literal, Protocol, TypeVar, dataclass_transformを組み合わせることで、Pythonでも高度な型安全が実現できる。
  • LLMやデータパイプラインではID・ステート・フォーマットが乱れがちなので、型で契約を表現し、CIで守らせる。
  • カスタムデコレータやDSLを使う場合は、mypyプラグインやdataclass_transformで型チェッカに意図を伝える。

「とりあえずdictAnyで書いてから直す」のではなく、最初に型の契約を敷き、AIコードもその枠内で生成・レビューする運用に切り替えましょう。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

この記事をシェア

続けて読みたい記事

編集部がピックアップした関連記事で学びを広げましょう。

#Python

型安全PythonでAIコードベースを堅牢化する実装ガイド

2025/11/23
#AI

AIガバナンス・プラットフォーム実装ガイド - Python・MLOps完全版【2025年最新】

2025/8/14
#CI/CD

CI/CD パイプライン遅延問題完全解決ガイド【2025年GitHub Actions最適化決定版】

2025/8/17
#Python

Pythonで株式投資分析!yfinance × pandas × pandas-taで実践するテクニカル分析入門

2025/11/26
#Mojo

Pythonの書きやすさでC++の速度を!AI開発言語「Mojo」入門【2025年版】

2025/11/26
#Python

Astral製uvとtyでPythonワークフローを再設計する

2025/11/23