【Python】Union の使い方とは?初心者向けにわかりやすく解説
はじめに
Python では、開発者がコードを読みやすくし、保守しやすくするためにタイプヒントを導入することが増えてきました。
その中でも Union は、複数の型をまとめて指定できる便利な仕組みです。
これを理解すると、実務でのエラーを減らし、チーム開発においても誰が見てもわかりやすいコードを書きやすくなります。
本記事では、プログラミング初心者を対象に Python の Union を使った型指定 について解説します。
具体的なコード例とともに、実務でどう活かすかも紹介しますので、ぜひ最後まで読んでみてください。
この記事を読むとわかること
- Union を用いたタイプヒントの基本的な書き方
- Union を実務で活かす際のメリットと利用シーン
- いくつかのコード例から学ぶ使いどころ
- Python のタイプヒント全般における Union の役割
- 実務でよくある Union 利用パターンと注意点
Python の型ヒントの役割
Python は動的型付け言語なので、変数の型を明示的に指定する必要がありません。
一方で、型を明示しないと、コードを読む側がどんな値を想定しているのかを把握しにくくなることがあります。
複数人での開発や大規模プロジェクトでは、誰が読んでもわかりやすいように型を指定するケースが多いです。
こうした観点でタイプヒントは多くの現場で活用されています。
実務におけるタイプヒントのメリット
タイプヒントを利用すると、IDE が型を推定しやすくなります。
そのため、コードを書いている段階で間違いに気づけたり、保守フェーズで仕様が把握しやすくなったりすることが多いです。
これによって以下のようなメリットが期待できます。
- 関数や変数の使い方を把握しやすくなる
- 思わぬ型エラーを事前に検知しやすくなる
- 新たにチームに参加した人でもコードの意図を理解しやすい
このように、タイプヒントは実務でも有用な仕組みです。
Union とは何か?
Union とは、複数の型を「どれか一つ」にまとめて表すための考え方です。
もしある変数が、文字列と数値のどちらかを受け取る可能性がある場合には、Union を使って明確に「文字列または数値」という注釈を付けることができます。
これによって、関数や変数の利用側も「この変数には文字列か数値しか入りません」と理解しやすくなり、安全にコードを追加・修正することができます。
Union を使わない場合の困りごと
Union を使わないと、例えばこんな状況が発生します。
def process_data(data): # data に何が入るのかがわからないので、利用するときに不安が生じる if isinstance(data, int): return data * 2 elif isinstance(data, str): return data.upper() else: return None
このコードでは、data
がいったいどの型になるのかが外から見るだけではわかりません。
文字列と数値のどちらも受け取る可能性があるという前提を知らない人からすると、使い方に戸惑うことになりがちです。
Union を使う場合の安心感
Union を使えば、以下のようにタイプヒントで示すことができます。
from typing import Union def process_data(data: Union[int, str]) -> Union[int, str, None]: if isinstance(data, int): return data * 2 elif isinstance(data, str): return data.upper() else: return None
このように記述すれば、関数の呼び出し側に「この関数は引数 data
に整数か文字列を期待しているんだな」といった情報が明確に伝わります。
また戻り値に関しても、int
か str
、もしくは None
を返すことがわかるので、呼び出し側のコードを書きやすくなります。
Union が活きる実務シーン
実務ではどういった場面で Union を使うと便利なのか、イメージがつきやすいようにいくつか例を挙げてみます。
API レスポンスを想定したデータ処理
例えば Web API から取得したデータが文字列で返ってくるか、数値で返ってくるかが環境や条件によって異なる場合があります。
API 通信の結果が文字列なのか数値なのかを実行時に動的に判別するケースは少なくありません。
こうしたときにも Union を使って型を明示すると、コードを読む人が「あ、データが文字列か数値のどちらかで返ってくる可能性があるんだな」とすぐにわかります。
データベースからの読み取り結果
データベースのカラム設計によっては、数値として扱うべきフィールドが状況によっては文字列になってしまうケースもあります。
例えば設定テーブルで、特定のステータスを文字列と数値のどちらで格納するかがプロジェクトの途中で変わったりすることも考えられます。
このような場合、Union で柔軟に表現しながら、関数内部では必要に応じて変換処理を行う方法が取りやすくなります。
ユーザー入力の想定
Webフォームやユーザーコンソール入力では、数字を入力したはずがプログラム内部では文字列として扱われることがあります。
たとえば「年齢」を入力してもらうフォームがあって、バックエンドが文字列でそのまま受け取るようになっている場合です。
「基本的には数値に変換して利用したいけれど、変換前の一時的な状態では文字列を保持している」というケースがあります。
こういったときにも Union が役立ちます。
Union とパイプ演算子による書き方
Python のタイプヒントを記述するにあたり、Union を使う方法には大きく二つの書き方があります。
Union[int, str]
のように書く方法int | str
のようにパイプ演算子を使う方法
いずれも複数の型を「どれかひとつ」と定義する考え方は同じです。
例:関数の引数で Union を使う
まずは Union[int, str]
を使う方法から見ていきましょう。
from typing import Union def show_length_or_double(data: Union[int, str]) -> None: if isinstance(data, int): print(data * 2) elif isinstance(data, str): print(len(data)) else: print("Unknown type")
このコードは int
であれば2倍した結果を表示し、str
であれば文字列の長さを表示しています。
単純な例ですが、実務においては「データが数値なら計算し、文字列なら別の処理をする」といった状況は意外とよくあります。
一方、同じことをパイプ演算子を使って書くと以下のようになります。
def show_length_or_double(data: int | str) -> None: if isinstance(data, int): print(data * 2) elif isinstance(data, str): print(len(data)) else: print("Unknown type")
大きく見た目が変わるわけではありませんが、読みやすさや簡潔さを好む人が多いため、パイプ演算子による記述方法を使うケースもあります。
Union のメリットとデメリット
どんな機能にもメリットとデメリットがあります。
Union も例外ではありません。
メリット
コードの意図が明確になる
関数や変数が複数の型を受け付けるときに、第三者がすぐ理解できるようになります。
IDE での補完や型チェックがしやすい
型推論が正確になり、開発効率が上がることがあります。
実装の自由度が増す
柔軟な設計がしやすくなり、特定の状況下では文字列、別の状況下では数値、というケースをカバーしやすくなります。
デメリット
型判断の分岐が多くなる
Union を使いすぎると、関数の中で if isinstance(x, int): ...
のような分岐が増えて複雑化する恐れがあります。
一貫性の取りづらさ
プロジェクト全体で統一したルールがないと、Union で定義した型が増えすぎて把握しにくくなることも考えられます。
他の仕組みを併用した方がよい場合もある
たとえばクラスを使ってそれぞれの型に応じた振る舞いをまとめるほうが、構造的にわかりやすい場面もあります。
Union の具体的な活用パターン
ここでは、実務で使いやすい具体的な活用パターンをいくつか紹介します。
クラス内の属性定義で使う
クラスを定義するとき、特定の属性が複数の型を受け取れる設計になる場合があります。
たとえば、ユーザー情報を扱うクラスで「名前」は文字列を想定しているけれど、システム上の都合で数値が入る可能性がある、といったケースです。
from typing import Union class UserData: def __init__(self, user_id: int, name: Union[int, str]): self.user_id = user_id self.name = name def show_info(self) -> None: if isinstance(self.name, int): print(f"Name as a number: {self.name}") elif isinstance(self.name, str): print(f"Name as a string: {self.name}")
このようにクラス内で Union を使っておくと、インスタンスを生成するときに何が期待されているかが明確です。
また、クラスメソッドや外部呼び出し時に「name
って文字列だよね」と思い込むのを防げます。
辞書構造を扱うときに使う
JSON 形式のデータを取り扱うとき、値が数値の時もあれば文字列の時もある、という状況が生じやすいです。
from typing import Union, Dict JsonDict = Dict[str, Union[int, str]] def process_json_data(data: JsonDict) -> None: for key, value in data.items(): if isinstance(value, int): print(f"{key} is an integer: {value}") else: print(f"{key} is a string: {value}")
上の例では簡単に書いていますが、実務で JSON の構造をタイプヒントで示す際に Union がよく活躍します。
たとえばキーごとに値の型が違う可能性があるなら、それぞれに応じてコードで分岐しながら処理を進められます。
関数の返り値としての Union
関数の返り値が複数のパターンを想定するときも、Union で整理するとわかりやすいです。
from typing import Union def parse_input(value: str) -> Union[int, str]: """ 文字列が数値に変換可能であれば int を返す それ以外の場合はそのまま文字列を返す """ try: return int(value) except ValueError: return value
このようにしておけば、呼び出し側は「戻り値が整数か文字列のどちらか」であることを踏まえて書けます。
たとえば、返り値が数値なら計算に使う、文字列ならエラーメッセージとして扱う、という分岐コードが自然な形で組めるようになります。
Union を使ううえでの注意点
Union は便利ですが、使い方を誤るとコードが複雑になったり、想定外の状況が混ざったりしやすくなることがあります。
ここでは、Union を使う上で意識しておきたい注意点をいくつか取り上げます。
使いすぎによる可読性の低下
Union は簡単に複数の型を混在できるため、多用すると「どの型が使われるのかよくわからない関数」ばかりになってしまう危険があります。
常に「本当に複数の型が必要なのか」「変換して一つの型にまとめられないか」を考えると、コードがスリムになりやすいです。
新しい型の増殖
Union の先にさらに別の Union を加えたり、入れ子状態にしてしまうと、把握がとても難しくなります。
Union[str, Union[int, Union[float, bool]]] # わかりにくい
このような書き方は避け、できるだけ単純な形にとどめるのがよいでしょう。
具体的なクラス設計との比較
特定の振る舞いやロジックが、型ごとにまったく異なる場合には、継承などを使ってクラス設計を行ったほうがわかりやすいかもしれません。
Union で複数の型を扱うよりも、抽象クラスを作って各型に応じたサブクラスで処理を実装した方が保守しやすい場面もあります。
実務での設計方針の考え方
Union を導入するかどうかを実務で判断する際には、以下のような観点を意識するとスムーズです。
1. チームのコーディング規約
プロジェクト全体でタイプヒントの書き方やルールが定められているかを確認しましょう。
2. 関数や変数がどの程度多用途か
どうしても複数の型を扱わなければならないのか、それとも設計上で型を統一する余地があるのかを考えます。
3. 可読性と保守性のバランス
コードを読む人が、Union のせいで混乱しないかどうかを確認します。
4. テストのしやすさ
テストコードを書く際に、複数の型を入力しなければならない場合はテストケースが増えます。
それが妥当なコストかどうかを検討します。
Union と Any・Optional の違い
Python のタイプヒントを利用するときに、Any や Optional など、他の型と合わせてどう使い分けるのか気になることがあるかもしれません。
Any
どんな型でも受け付ける、いわば「なんでもアリ」の型。
これは型安全を守りにくくなる可能性があるので、なるべく必要最小限に留めるとよいです。
Optional
値が「あるか、ないか」を示すときに使います。
厳密には Union[型, None]
のシンタックスシュガーとも言われますが、用途がはっきりしているので Optional[type]
の形で書くことが多いです。
Union は複数の型をまとめるので、Optional[int]
は Union[int, None]
と等価ですが、読み手の理解を助けるために適切に使い分けることが多いです。
具体的なコード例:ファイル入力での処理
ここでは、実際の実務でありがちなファイル入力の例を挙げてみます。
ユーザーがアップロードしてきたファイルの拡張子によって処理を分けたいけれど、拡張子がうまく取得できない場合や、拡張子が意図せず数値として読み取られる場合などを想定します。
from typing import Union def handle_file_data(filename: str) -> Union[str, int, None]: """ ファイル拡張子を判定し、特定の条件に合致すれば数値を返す。 取得できない場合は None を返す。 条件に合わなければ文字列を返す。 """ parts = filename.split(".") if len(parts) < 2: # 拡張子なし return None ext = parts[-1] # 何か特殊なルールで拡張子が数字とみなせる場合は int に変換して返す if ext.isdigit(): return int(ext) # それ以外は拡張子を文字列として返す return ext
このようにすると、「拡張子が存在しない場合」「拡張子が数字の場合」「拡張子が通常の文字列の場合」でそれぞれ異なる型を返すコードになります。
Union を使うことで、呼び出し側も戻り値が「str
か int
か None
」であると把握できます。
実務視点で見るテストコード
上の関数 handle_file_data()
をテストするとき、以下のようにテストケースを用意できるでしょう。
example.txt
→"txt"
が返る想定2023
→None
(拡張子がない)document.123
→123
(数値として返る)testfile.
→ 空の拡張子を検出してNone
が返るsample.json.zip
→"zip"
が返る
こうしたテストコードを書くときも、Union が明確になっているおかげで「どのパターンをテストすればいいか」が一目でわかりやすいです。
Union とジェネリクスの組み合わせ
Python ではジェネリクスを活用して、あるコレクションの要素の型を注釈することができます。
その際に要素が複数の型を取りうる場合、ジェネリクスと Union を組み合わせることもあります。
from typing import Union, List, TypeVar T = TypeVar("T", int, str) def process_items(items: List[T]) -> None: for item in items: if isinstance(item, int): print(f"Integer found: {item}") elif isinstance(item, str): print(f"String found: {item}")
ここでは List[T]
の要素が int
と str
のいずれかを受け取れるようにしています。
実際には Union[int, str]
のリストとして指定することも可能ですが、ジェネリクスを使うとより柔軟な設計を行えるケースがあります。
複数人での開発を想定したコード管理のポイント
Union を使い始めると、チームメンバー同士でどのようにコードを管理するかが課題になることがあります。
以下のようなポイントに留意して設計すると、保守しやすいプロジェクトにしやすいでしょう。
共通の型定義ファイルを用意する
例えば types.py
のようなファイルを作成し、そこでよく使う Union を定義しておく。
ドキュメント化
「こういった意図で Union を使っています」という説明をチーム内で共有する。
ドキュメントを整備しておくと、新規参加者が混乱しにくいです。
型チェッカーの導入
mypy
や pyright
といった型チェッカーを利用し、Union の指定が開発の流れを止めるほど厄介になっていないかを常にチェックするとよいです。
実行パフォーマンスへの影響
Python のタイプヒントはあくまでもヒントであって、実行時のパフォーマンスに直接影響するわけではありません。
ただし、Union を使ったコードでは、実行時に isinstance()
チェックが多用されることもあります。
これはタイプヒントとは関係なく「複数の型を扱うならチェックが増える」という構造的な問題です。
極端な数の型を許容してしまうと、それだけ分岐が多くなるため、コードは複雑化する可能性があります。
パフォーマンスよりも可読性や保守性の観点で、Union をバランスよく使うことが重要です。
Python Union を導入するステップ
もしこれからプロジェクトで Union を導入してみようと考えているなら、以下のようなステップで進めてみてはいかがでしょうか。
1. まずは小規模な関数から
すでに複数の型を受け取っている関数に Union を付与してみる。
実際に型チェッカーを通してみて、不備がないか確認します。
2. 戻り値やクラス属性にも適用
「引数にも型が付いているけど、戻り値やクラス属性には型がない」などの不均衡があるなら、そこも整備してみる。
3. 複雑になりそうな箇所を再設計
Union を付けたらかえって複雑になった場所があれば、クラス化や別の設計に置き換えたほうが読みやすいかどうかを検討します。
4. チームメンバーとのすり合わせ
「Union を使うかどうか」「複数の型が必要なときのルール」を取り決めておくことで、コードレビューがスムーズになります。
特定の状況で型が明確に一つに定まるはずなのに、実装上の都合だけで Union を使うのは避けたほうが無難です。 明確に型が決まっている箇所には、その型をしっかり指定したほうが、後々の変更コストを下げられます。
Python Union を扱うときのよくある疑問
初心者の方が Union を学ぶ際によく抱く疑問点をいくつか取り上げてみます。
Union[int, str] と int | str はどちらを使えばいいですか?
好みやプロジェクトの規約によります。
どちらも同じ意味で書けますが、パイプ演算子を使ったほうが記述が短くなるため、パイプ演算子の方を好む人がいます。
プロジェクトのコーディング規約に合わせるのがよいでしょう。
Union の型をチェックする方法は?
実行時には isinstance()
を使って個別にチェックすることになります。
コード補完などで型を識別するのは IDE や型チェッカーが裏で行っているので、実行パフォーマンスとは無関係です。
すでに動いているコードに後から Union を付けてもいいですか?
もちろん可能です。
ただし、大規模なコードの場合は一気に付け替えると混乱を招く場合があります。
小さな部分から導入してみて、テストがしっかり通ることを確認しながら進めるほうが安心です。
Union はサードパーティライブラリをインストールしないと使えない?
いいえ、Python に標準で含まれる typing
モジュールを使うだけで OK です。
特別なライブラリを追加導入する必要はありません。
まとめ
本記事では Python の Union について、初心者の方でもわかりやすいように解説してきました。
Union は複数の型をまとめて指定できるため、実務のさまざまな場面で役立ちます。
例えば Web API から得られるデータや、ユーザー入力のデータが複数の型になる可能性があるときなどに、Union を使ってコードの意図をはっきり示すことができます。
一方で、多用しすぎると分岐処理が増え、コードが複雑になりやすい側面もあります。
そのため、「本当に複数の型が必要か」を考えながら導入することがポイント です。
全体としては、Python で型ヒントを活用し、チーム開発や大規模開発でもメンテナンスしやすいコードを書くために、ぜひ今回の Union の知識を役立ててみてください。