できない.dev

Python で「'NoneType' object has no attribute」が解消できない

AttributeError の対象が NoneType の場合、メソッドの戻り値や辞書の get、ライブラリ API が想定外に None を返している。
値が None である可能性を設計で潰すか、is None で早期判定してから属性アクセスする。

#python#attributeerror#nonetype#exception#type-hint

要約

'NoneType' object has no attribute 'xxx' は、None に対して属性アクセスした ことを示す。
直前のメソッド呼び出しや関数の戻り値が想定と違って None になっていないかを最初に疑う。is None で早期判定するか、そもそも None を返さない型設計にする。

よくある原因

  1. インプレース操作の戻り値を代入: lst = lst.append(x)s = s.sort()None を返す。
    Python のミュータブル系メソッドの典型的な罠。
  2. 既定値なしの get: os.environ.get("X")re.match(pat, s) は値が無い・マッチしないと None
    続けて .group() などを呼ぶと例外。
  3. 関数の return 忘れ: def f(): result = compute() のように return を書き忘れていると、呼び出し側で f().attr が落ちる。
  4. API の None 返却: Django ORM qs.first()requests のオプショナル属性、SQLAlchemy の session.get() などは見つからないと None

解決策

1. インプレースと関数型を見分ける

# NG: append は None を返す
items = items.append(x)
 
# OK: 戻り値を使わない or 関数型版を使う
items.append(x)
items = items + [x]

2. is None で早期判定

m = re.match(r"(\d+)", s)
if m is None:
    return 0
return int(m.group(1))

公式ドキュメント のとおり、None に属性は無い前提で書く。

3. typing.Optional と型チェッカ

from typing import Optional
 
def find_user(uid: int) -> Optional["User"]:
    ...
 
user = find_user(1)
if user is None:
    raise LookupError
user.name   # ここでは User 型に絞られている

mypy / pyright を導入すれば、typing.Optional の取り扱い漏れを CI で潰せる。

4. 既定値や raise する版を選ぶ

env = os.environ.get("KEY", "")    # 既定値
env = os.environ["KEY"]            # 無ければ KeyError
user = User.objects.get(pk=1)      # 無ければ DoesNotExist

「無いときに静かに None で返す API」と「raise する API」を場面で使い分けると、後段で属性アクセスする側の不安が消える。

この記事は役立ちましたか?