意図的にエラーを発生させるraise
は、入力チェックや未実装機能の通知などでプログラムの意図を明確にできる強力な仕組みです。
正しい場面で正しい例外を発生させることは、学習初期から身につけたい重要スキルです。
本記事では、Python初心者の方に向けてraise
の基本と実例、再送出の注意点まで丁寧に解説します。
Pythonのraiseとは?意図的に例外を発生させる基本
raiseの役割
raise
は、プログラムの実行を中断して「異常が起きた」ことを上位へ伝えるための文です。
外部からの入力や前提条件が守られていない場合に、明示的に例外を投げることで、バグの早期発見や原因箇所の特定が容易になります。
結果として、問題を曖昧に先送りするよりも堅牢なコードになります。
どんな時に使うのか
入力値が規定範囲外の場合や、未対応の機能が呼ばれた場合、または辞書のキーが存在しない場合など、想定通りに処理できない状況で使います。
「この先へ進むべきではない」ことを明示する合図だと考えてください。
最小の例
# 意図的にValueErrorを発生させる最小の例
def positive_square_root(x: float) -> float:
if x < 0:
# 期待通りに計算できないので例外を投げる
raise ValueError("xは0以上である必要があります")
return x ** 0.5
print(positive_square_root(9))
print(positive_square_root(-1)) # ここで例外が発生
3.0
Traceback (most recent call last):
File "...", line ..., in <module>
print(positive_square_root(-1))
File "...", line ..., in positive_square_root
raise ValueError("xは0以上である必要があります")
ValueError: xは0以上である必要があります
基本構文(例外クラスとメッセージ)
構文はraise 例外クラス(エラーメッセージ)
が基本です。
エラーメッセージは、原因がすぐ分かる具体的な文にします。
構文のバリエーション
- 標準的:
raise ValueError("理由")
- 変数に格納した例外を投げる:
err = ValueError("..."); raise err
- 原因の例外を連鎖させる:
raise RuntimeError("外側の説明") from inner_exc
(後述)
# 基本構文の例: 型と値の両方をチェック
def normalized_ratio(a: float, b: float) -> float:
# 型チェック(ここではfloatまたはintを期待)
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("aとbは数値(int/float)である必要があります")
# 値チェック(分母は0不可)
if b == 0:
raise ValueError("b(分母)は0ではいけません")
return a / b
print(normalized_ratio(3, 2))
print(normalized_ratio("3", 2)) # TypeError
1.5
Traceback (most recent call last):
File "...", line ..., in <module>
print(normalized_ratio("3", 2)) # TypeError
File "...", line ..., in normalized_ratio
raise TypeError("aとbは数値(int/float)である必要があります")
TypeError: aとbは数値(int/float)である必要があります
よくある誤り(文字列はraiseできない)
Python 3では文字列そのものをraise
できません。
必ず例外クラス(例: ValueError)のインスタンスを投げます。
# NG例: 文字列をraiseしてはいけない
def bad():
raise "エラー" # これはPython 3ではTypeErrorになります
bad()
Traceback (most recent call last):
File "...", line ..., in <module>
bad()
File "...", line ..., in bad
raise "エラー"
TypeError: exceptions must derive from BaseException
# OK例: 適切な例外クラスを使う
def good():
raise ValueError("エラー: 入力が不正です")
good()
Traceback (most recent call last):
File "...", line ..., in <module>
good()
File "...", line ..., in good
raise ValueError("エラー: 入力が不正です")
ValueError: エラー: 入力が不正です
例外クラスの選び方(初心者向け)
初心者のうちは、標準の例外クラスから適切なものを選ぶのが最短ルートです。
迷った時は「何が問題だったのか」を言い表す名前の例外を選び、説明的なメッセージを添えると伝わりやすくなります。
以下は代表的な選択基準です。
例外クラス | 使う場面 | 例メッセージ |
---|---|---|
ValueError | 値の範囲や形式が不正 | “ageは0以上の整数である必要があります” |
TypeError | 引数や戻り値の型が違う | “pathはstr型である必要があります” |
KeyError | 辞書や環境変数にキーがない | “必須設定KEYが見つかりません: ‘API_KEY'” |
NotImplementedError | 未対応・未実装の機能が呼ばれた | “このフォーマットは未対応です: ‘xml'” |
RuntimeError | 実行時の前提条件違反 | “接続前にsend()は呼べません” |
ValueError(不正な値)
値の範囲や形式が不正な場合はValueError
です。
例えば負の数や空文字など、型は正しいが中身が不正な時に使います。
def parse_age(text: str) -> int:
# 空文字や数字以外を弾く
if not text or not text.isdigit():
raise ValueError(f"ageの形式が不正です: {text!r}")
age = int(text)
if age < 0:
raise ValueError("ageは0以上である必要があります")
return age
print(parse_age("20"))
print(parse_age("abc")) # ValueError
20
Traceback (most recent call last):
File "...", line ..., in <module>
print(parse_age("abc")) # ValueError
File "...", line ..., in parse_age
raise ValueError(f"ageの形式が不正です: {text!r}")
ValueError: ageの形式が不正です: 'abc'
TypeError(型が違う)
期待した型と違う時はTypeError
を使います。
型ヒントと組み合わせると原因が明確になります。
from typing import Iterable
def head(items: Iterable, n: int = 1):
if not isinstance(n, int):
raise TypeError("nはint型である必要があります")
return list(items)[:n]
print(head([1, 2, 3], n="2")) # TypeError
Traceback (most recent call last):
File "...", line ..., in <module>
print(head([1, 2, 3], n="2")) # TypeError
File "...", line ..., in head
raise TypeError("nはint型である必要があります")
TypeError: nはint型である必要があります
KeyError(キーがない)
辞書や環境変数に欲しいキーがない場合はKeyError
が適切です。
データ欠損を明確に示せます。
settings = {"host": "localhost"}
def get_required(config: dict, key: str):
if key not in config:
raise KeyError(f"必須設定が不足しています: {key!r}")
return config[key]
print(get_required(settings, "port")) # KeyError
Traceback (most recent call last):
File "...", line ..., in <module>
print(get_required(settings, "port")) # KeyError
File "...", line ..., in get_required
raise KeyError(f"必須設定が不足しています: {key!r}")
KeyError: "必須設定が不足しています: 'port'"
NotImplementedError(未対応の機能)
未対応や未実装の機能が呼ばれたらNotImplementedError
です。
注意: NotImplemented
(特殊な値)とNotImplementedError
(例外)は別物です。
class Exporter:
def export(self, fmt: str) -> str:
if fmt == "json":
return '{"ok": true}'
# まだ対応していない形式
raise NotImplementedError(f"このフォーマットは未対応です: {fmt!r}")
exp = Exporter()
print(exp.export("xml")) # NotImplementedError
Traceback (most recent call last):
File "...", line ..., in <module>
print(exp.export("xml")) # NotImplementedError
File "...", line ..., in export
raise NotImplementedError("このフォーマットは未対応です: 'xml'")
NotImplementedError: このフォーマットは未対応です: 'xml'
RuntimeError(前提条件を満たさない)
操作の順序や状態が不適切な時はRuntimeError
が選択肢です。
接続前の送信など、値や型の問題ではない「状態」の問題に向いています。
class Connection:
def __init__(self):
self.connected = False
def connect(self):
self.connected = True
def send(self, data: bytes):
if not self.connected:
raise RuntimeError("接続前にsend()は呼べません")
print("送信:", data)
conn = Connection()
conn.send(b"hello") # RuntimeError
Traceback (most recent call last):
File "...", line ..., in <module>
conn.send(b"hello") # RuntimeError
File "...", line ..., in send
raise RuntimeError("接続前にsend()は呼べません")
RuntimeError: 接続前にsend()は呼べません
実例で学ぶraiseの使い方
入力チェックでValueErrorをraise
関数の入口でガードして、誤入力を早期に検出します。
後段でおかしな挙動になるより、入口で止める方が安全です。
def percentage(text: str) -> float:
"""'85%'のような文字列を0.0〜1.0の小数へ変換"""
if not text.endswith("%"):
raise ValueError("末尾に%が必要です (例: '85%')")
num = text[:-1]
if not num.replace(".", "", 1).isdigit():
raise ValueError(f"数値部分が不正です: {num!r}")
value = float(num) / 100.0
if not (0.0 <= value <= 1.0):
raise ValueError("0%〜100%の範囲で指定してください")
return value
print(percentage("85%"))
print(percentage("120%")) # 範囲外 → ValueError
0.85
Traceback (most recent call last):
File "...", line ..., in <module>
print(percentage("120%")) # 範囲外 → ValueError
File "...", line ..., in percentage
raise ValueError("0%〜100%の範囲で指定してください")
ValueError: 0%〜100%の範囲で指定してください
未対応の処理でNotImplementedErrorをraise
ロードマップ上で未対応の分岐を明示すると、呼び出し側は安全にフォールバックできます。
def export(data: dict, fmt: str) -> str:
if fmt == "json":
import json
return json.dumps(data)
elif fmt == "csv":
# TODO: 後で実装
raise NotImplementedError("csv出力はまだ未対応です")
else:
raise NotImplementedError(f"未知の形式です: {fmt!r}")
print(export({"x": 1}, "csv"))
Traceback (most recent call last):
File "...", line ..., in <module>
print(export({"x": 1}, "csv"))
File "...", line ..., in export
raise NotImplementedError("csv出力はまだ未対応です")
NotImplementedError: csv出力はまだ未対応です
ラッパー関数でKeyErrorをraise
外部APIや辞書アクセスのラッパーで欠損キーをKeyError
で伝えると、呼び出し側は例外種別で分岐できます。
import os
def get_env_required(name: str) -> str:
"""必須な環境変数を取得。なければKeyErrorを投げる。"""
try:
return os.environ[name]
except KeyError as e:
# 独自メッセージに差し替えて再送出
raise KeyError(f"必須の環境変数が見つかりません: {name!r}") from e
print(get_env_required("API_KEY")) # 未設定ならKeyError
Traceback (most recent call last):
File "...", line ..., in get_env_required
return os.environ[name]
KeyError: 'API_KEY'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "...", line ..., in <module>
print(get_env_required("API_KEY")) # 未設定ならKeyError
File "...", line ..., in get_env_required
raise KeyError(f"必須の環境変数が見つかりません: {name!r}") from e
KeyError: "必須の環境変数が見つかりません: 'API_KEY'"
Noneの返却ではなく例外で失敗を示す
失敗をNone
で返すと、バグに気づきにくくなります。
失敗は例外で明確に伝える方が安全です。
サンプルコード(悪い例と良い例)
# 悪い例: 失敗時にNoneを返す
def parse_int_or_none(text: str):
return int(text) if text.isdigit() else None
x = parse_int_or_none("abc")
# xがNoneだとこの後の処理でTypeErrorなどの二次被害を生むことがある
print(x + 1) # ここでエラー: TypeError
Traceback (most recent call last):
File "...", line ..., in <module>
print(x + 1)
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
# 良い例: 失敗時は明確にValueErrorを投げる
def parse_int(text: str) -> int:
if not text.isdigit():
raise ValueError(f"整数として解釈できません: {text!r}")
return int(text)
# 呼び出し側はtry-exceptで意図を持って対処できる
try:
print(parse_int("abc"))
except ValueError as e:
print("入力エラー:", e)
入力エラー: 整数として解釈できません: 'abc'
例外の再送出(re-raise)と注意点
try-except内でraiseだけを書く
捕捉した例外をそのまま上に投げ直すときはraise
だけを書くのが基本です。
これにより元のトレースバック(発生箇所)が保たれます。
def outer():
try:
int("abc") # ここでValueErrorが発生
except ValueError:
# ローカルでは何もせず、そのまま再送出
raise
outer()
Traceback (most recent call last):
File "...", line ..., in outer
int("abc") # ここでValueErrorが発生
ValueError: invalid literal for int() with base 10: 'abc'
ログを記録してから再送出
ログを残しつつ例外を上位へ伝えたいときはlogging.exception()
とraise
を組み合わせます。
logging.exception()
はexcept節内で使うとスタックトレース付きでERRORログを出力します。
import logging
logging.basicConfig(level=logging.ERROR, format="%(levelname)s:%(message)s")
logger = logging.getLogger(__name__)
def work():
try:
return 10 / 0
except Exception:
logger.exception("計算に失敗しました")
raise # 元の例外(ZeroDivisionError)を保ったまま再送出
work()
ERROR:計算に失敗しました
Traceback (most recent call last):
File "...", line ..., in work
return 10 / 0
ZeroDivisionError: division by zero
Traceback (most recent call last):
File "...", line ..., in <module>
work()
File "...", line ..., in work
raise # 元の例外(ZeroDivisionError)を保ったまま再送出
ZeroDivisionError: division by zero
例外情報を失わないコツ
「どこで何が起きたか」を失わないことが、デバッグ効率を大きく左右します。
特にraise e
は元のトレースバックを失うので注意してください。
1) そのまま再送出する時はraiseのみ
def re_raise():
try:
float("NaN!") # ValueError
except ValueError:
raise # OK: 元のトレースバックを保持
2) NGパターン: raise e はトレースバックが現在位置にリセットされる
def lose_traceback():
try:
float("NaN!") # ここが本当の発生源
except ValueError as e:
raise e # NG: トレースバックがこの行から始まってしまう
実行結果(概念的には「発生源の行」が表示されにくくなる)ため、原因特定が難しくなります。
3) 新しい文脈を与える時は raise NewError(…) from e
raise 新しい例外 from 元の例外
で「原因の例外」を連鎖できます。
これにより、「上位の文脈の説明」と「下位の真の原因」の両方が一つのトレースバックに含まれ、解析が容易です。
def wrap_with_context():
try:
int("xyz") # ValueError
except ValueError as e:
# より上位の文脈を説明する例外に包んで連鎖
raise RuntimeError("ユーザーIDの解析に失敗しました") from e
wrap_with_context()
Traceback (most recent call last):
File "...", line ..., in wrap_with_context
int("xyz") # ValueError
ValueError: invalid literal for int() with base 10: 'xyz'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "...", line ..., in <module>
wrap_with_context()
File "...", line ..., in wrap_with_context
raise RuntimeError("ユーザーIDの解析に失敗しました") from e
RuntimeError: ユーザーIDの解析に失敗しました
4) 内部原因を隠したい時は from None
外部に内部の実装詳細を漏らしたくない場合は、raise SomeError("公開メッセージ") from None
で原因の連鎖表示を抑止できます。
def hide_internal():
try:
open("/root/secret.txt")
except OSError:
# 内部のパスなどを隠して公開用のメッセージに限定
raise PermissionError("許可されていない操作です") from None
hide_internal()
Traceback (most recent call last):
File "...", line ..., in <module>
hide_internal()
File "...", line ..., in hide_internal
raise PermissionError("許可されていない操作です") from None
PermissionError: 許可されていない操作です
まとめると:
- そのまま再送出 →
raise
だけ - 文脈を付けて伝える →
raise NewError(...) from e
- 原因を隠す →
from None
- 避けるべき →
raise e
まとめ
raise
は「この条件では処理を続けられない」という意図をコードで正確に表現するための基礎テクニックです。
値が不正ならValueError
、型が違えばTypeError
、キーがなければKeyError
、未対応はNotImplementedError
、状態不整合ならRuntimeError
と、適切な例外を選ぶことで、原因が読み取れるエラーメッセージを実現できます。
失敗をNone
でごまかさず、例外で明確に伝える姿勢が品質を高めます。
再送出ではraise
だけで元のトレースバックを保ち、文脈追加はraise ... from e
を活用しましょう。
今日から小さな関数でもraise
を積極的に使い、読みやすく堅牢なコードへと改善していってください。