0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonコーディングの落とし穴63選:実務で使えるベストプラクティス集

0
Last updated at Posted at 2026-04-18

Pythonの基礎から応用にかけて、開発者が陥りやすい「よくある落とし穴・間違い(アンチパターン)」と、その解決策(ベストプラクティス)をを説明します。

0. 基本データ型 (Basic Data Types)

  • boolint のサブクラスのため、True == 1False == 0 が成立します。
is_ready = True
True + True       # 2  -> bool は int のサブクラス(subclass)。これは典型的な落とし穴
  • 浮動小数点数(float)は精度誤差があるため、直接的な一致比較(==)は避けるべきです(math.isclose() を推奨)。
ratio = 0.1 + 0.2
ratio             # 0.30000000000000004 -> 浮動小数点数の精度問題(precision issue)
  • None の判定は、== None ではなく is None を使用してください。
result = None
result is None    # True  -> None の判定には `is` を推奨
  • 変数への代入は「複製(コピー)」ではなく、「同一オブジェクトへの参照」です。
# 代入はコピーではない
a = [1, 2]
b = a
c = a.copy()

a.append(3)
b   # [1, 2, 3]  -> b と a は同じオブジェクトを参照している
c   # [1, 2]     -> c は浅いコピー(shallow copy)
  • tuple 自体は不変(immutable)ですが、内部に可変(mutable)オブジェクト(リストなど)を含んでいる場合、その中身は変更可能です。
# tuple 自体は不変だが、内部の可変オブジェクトは変更可能
t = ([1, 2], 'fixed')
t[0].append(99)
t   # ([1, 2, 99], 'fixed')
  • set は順序がない(unordered)ため、インデックスによる要素へのアクセスはできません。
# set はインデックスでアクセスできない
nums = {1, 2, 3}
# nums[0]        # TypeError: 'set' object is not subscriptable
  • dict のキーや set の要素にするオブジェクトはハッシュ可能(hashable)である必要があります。
# dict のキーはハッシュ可能(hashable)である必要がある
good = {(1, 2): 'point'}      # tuple はキーにできる
# bad = {[1, 2]: 'point'}     # TypeError: unhashable type: 'list'

1. コア構文と演算子 (Core Syntax & Operator)

  • 割り算 1 / 2 は浮動小数点の 0.5 を返します。切り捨て除算(整数)を求める場合は // を使用します。
10 / 3            # 3.3333333333333335  -> 通常の割り算
10 // 3           # 3                   -> 切り捨て除算(floor division)
  • bool('0') は空文字ではないため True となります。
# 真偽値(truthiness)
bool(0)       # False -> 0 は偽
bool('0')     # True  -> 空ではない文字列は真
  • 0 == False は値としては真(True)ですが、両者のデータ型は異なります。
0 == False    # True
0 is False    # False -> 同一オブジェクト(same object)ではない
  • andor は必ずしも bool を返すとは限りません。短絡評価(short-circuit evaluation)により、最後に評価された値自体が返されます。
# and / or の戻り値
[] or ['fallback']        # ['fallback'] -> 左辺が偽のため、右辺を返す
'hello' or 'world'        # 'hello'      -> 左辺が真のため、そのまま左辺を返す
0 and 99                  # 0            -> 左辺が偽のため、そのまま左辺を返す
  • is は主にオブジェクトの同一性の確認(identity comparison:特に None やシングルトンの判定)に用い、一般的な「値の等価性(equality)」の判定として使うべきではありません。
# == vs is
left = [1, 2]
right = [1, 2]
alias = left

left == right   # True  -> 内容が等しい(equal in value)
left is right   # False -> 同一オブジェクト(same object)ではない
left is alias   # True  -> alias と left は同じオブジェクトを参照
  • 1 < x < 10 のようなチェーン比較は有効で、「両方の条件が同時に満たされること」を意味します。
# チェーン比較(chained comparison)
x = 5
1 < x < 10                # True -> 1 < x and x < 10 と等価

2. 制御構文とループ (Control Flow & Loop)

  • range(5) の範囲は [0, 1, 2, 3, 4] であり、末尾の 5 は含まれません(左閉右開:half-open interval)。
# for + range:最も基本的なカウントループ
total = 0
for i in range(1, 6):       # 1 から 5 まで。6 は含まれない
    total += i
  • while ループ内でループ条件変数の更新を忘れると、無限ループに陥ります。
# while:終了回数が不明なループに適している
x = 1
while x < 20:
    x *= 2                   # x の更新を忘れると無限ループに陥る恐れがある
x                            # 32
  • break は現在の「最も内側のループ1つ」のみを抜け出します。多重ループ全体を終了するわけではありません。
# ネストされたループの場合、break は内側のループのみを終了させる
pairs = []
for i in range(2):
    for j in range(3):
        if j == 1:
            break            # ここでは内側の for j のみ終了する
        pairs.append((i, j))
pairs                        # [(0, 0), (1, 0)]
  • 可変リストを for で反復処理しながら中身を削除・変更すると、要素のスキップなど予期せぬ動作を引き起こします。
nums = [1, 2, 3, 4]
# ループしながら削除すると、インデックスがずれて隣の要素がスキップされてしまう。
# リスト内包表記などで新しいリストを作成するのが適切。
valid_nums = [n for n in nums if n != 2]
  • Python特有の for ... else / while ... elseにおける else ブロックは、「条件に合わなかった時」ではなく「ループが break されずに途切れることなく完走した時」のみ実行されます。
# for-else:break されなかった場合のみ else に入る
num = 29
for d in range(2, int(num ** 0.5) + 1):
    if num % d == 0:
        print('not prime')    # 約数が見つかったときだけ出力される
        break
else:
    print('prime')            # prime  29は素数(prime number)
  • if x: では、空文字列、空リスト、0 なども「偽」と判定されます。これが意図せぬ場合(値自体が None かどうか判定したい場合)は、if x is not None: と書く必要があります。
# 真偽値のよくある落とし穴
x = 0
if x: pass # 0 は False と見なされるため、ここには入らない
if x is not None: pass # ここには入る。非 None のみを判定したい場合に適している

3. 文字列処理とエンコーディング (Strings & Encoding)

  • input() で受け取った入力はすべて str 型です。数値として扱うにはキャスト(int() など)が必要です。
age_text = '18'
age = int(age_text)       # str -> int
  • strip('abc') は完全な部分文字列「'abc'」を削るのではなく、先頭と末尾にある「'a', 'b', 'c'の全文字」を取り除きます。
# strip の意味に注意
text = 'abcHelloCab'
text.strip('abc')            # 'HelloC' -> 先頭と末尾にある a/b/c の文字を削除。「部分文字列」という概念は区別しない
  • replace(), upper(), lower() 等のメソッドは元の文字列を変更(破壊的変更)せず、新たな文字列を戻り値として返します。
# replace は新しいオブジェクトを返す
msg = 'I like Java'
new_msg = msg.replace('Java', 'Python')
msg                          # 'I like Java' -> 元の文字列はそのまま
new_msg                      # 'I like Python'
  • ord() 関数は1文字(単一文字)しか受け付けません。数字以外の文字のコードポイントを取得する場合は int('A') ではなく ord('A') を使います。
# 文字と数値間の変換
ord('A')                     # 65 -> 文字をコードポイントに変換
chr(65)                      # 'A' -> コードポイントを文字に変換

# ord は単一の文字しか処理できない
# ord('AB')                  # TypeError になる
  • str(テキスト)と bytes(バイト列)は直接連結できません。
# encode / decode:テキスト <-> バイト列
text = 'cafe'
data = text.encode('utf-8')  # str -> bytes
data                         # b'cafe'

decoded = data.decode('utf-8')
decoded                      # 'cafe' -> bytes -> str
  • 引数なしの split() はすべての連続する空白文字類(空白文字、タブ文字、改行文字など)を区切りにしますが、split(',') のように指定した場合はその文字のみを基準にするため、空文字がリストに含まれるなど挙動が異なります。
# split / join
line = 'name,age,city'
parts = line.split(',')      # ['name', 'age', 'city']

# デフォルトの split は任意の連続する空白文字で分割する
sentence = 'too    many\tspaces'
sentence.split()             # ['too', 'many', 'spaces']

4. コンテナとアルゴリズム基礎 (Containers & Basic Algorithms)

  • a = b による代入はコピーを作成しません。独立した別のオブジェクトを作りたい場合は .copy() などが必要です。
# コピーの落とし穴(copy pitfall)
a = [1, 2]
b = a
c = a.copy()
a.append(3)
b                            # [1, 2, 3] -> b も追従して変わる
c                            # [1, 2]    -> c は変わらない
  • remove(x) は値としての一致要素を削除し、pop(i) はインデックスに基づく要素の取得と削除を行います。
nums = [3, 1, 2, 2]
nums.append(5)               # 末尾に追加
nums.pop()                   # 最後の要素を削除して返す -> 5
  • set は順序がないためインデックスアクセス(s[0] など)は不可能です。
user_ids = {'u001', 'u002', 'u002'}
user_ids                     # {'u001', 'u002'} -> 自動で重複排除
'u001' in user_ids           # True -> 所属判定(membership test)は非常に高速
# user_ids[0]                # これを実行すると TypeError になる
  • 辞書のキーが存在するかわからない場合、直接アクセスする dict['missing'] は例外(KeyError)を送出してしまいます。安全に取得するには dict.get('missing') を使用してください。
# dict: key-value のマッピング
scores = {'ann': 95, 'bob': 88}
scores['ann']                # 95
scores.get('cindy', 0)       # 0 -> KeyError を投げない
  • 2次元リストの初期化において [[0] * 3] * 2 と記述すると、各行が同じリストの参照を共有してしまい、1箇所の変更が他の行にも連動してしまいます。
# 2次元リスト初期化の落とし穴
bad = [[0] * 3] * 2
bad[0][1] = 99
bad                          # [[0, 99, 0], [0, 99, 0]] -> 両方の行が同時に変わる

good = [[0] * 3 for _ in range(2)]
good[0][1] = 99
good                         # [[0, 99, 0], [0, 0, 0]] -> 正しい書き方
  • 二分探索(バイナリサーチ)は、データが「事前にソート(整列)されていること」が絶対条件です。
# 二分探索(binary search):入力がソート済みであることが前提
def binary_search(sorted_items, target):
    pass
# 常にソートされている必要がある
binary_search([1, 3, 5, 7, 9], 7)   # 3
  • sorted() は安定ソート(stable sort)であるため、複数条件でソートを重ねる際に役立ちます。
# ソート:key で並べ替え
rows = [
    {'name': 'ann', 'age': 22},
    {'name': 'bob', 'age': 19},
    {'name': 'cindy', 'age': 25},
]
sorted(rows, key=lambda row: row['age'])
# age が小さい順にソートされる

5. 関数、スコープと状態の引き渡し (Functions,Scope & Passing State)

  • 戻り値の指定(return)がない関数は、自動的に None を返します(print しただけでは戻り値はありません)。
def my_func():
    print("hello") # 出力のみで、return はない

result = my_func()
print(result) # None 
  • リストや辞書などの可変オブジェクトを関数の「デフォルト引数」として設定(例:def func(acc=[]))してはいけません。関数が呼び出されるたびに同じオブジェクトが使い回されてしまいます。
# 可変デフォルト引数の落とし穴に対する正しい書き方
def append_item(x, bucket=None):
    if bucket is None:       # 呼び出されるたびに新しいリストを作成
        bucket = []
    bucket.append(x)
    return bucket

append_item(1)               # [1]
append_item(2)               # [2] -> 前回のリストを引き継がない
  • リストや辞書などの可変オブジェクト(特に可変コンテナ)を引数として渡し、関数内でインプレース変更を行うと、呼び出し元のオブジェクトにも変更が反映されます。
# 可変引数が関数内部で変更される例
def add_tag(tags, tag):
    tags.append(tag)         # 外部のリストをインプレースに変更(in-place mutation)する
    return tags

skills = ['python']
add_tag(skills, 'sql')
skills                       # ['python', 'sql'] -> 外部側のリストも変わっている
  • リストをインプレースでソートする list.sort() は戻り値が None です。そのため a = a.sort() と書くと a 自体が消失(None)します。
nums = [3, 1, 2]
# result = nums.sort()   # 落とし穴:このとき result の値は None になる
sorted_nums = sorted(nums)   # 新しいリストを返す
nums                         # 元のリストはそのまま存在
sorted_nums                  # ソート済みの新しいリスト
  • 再帰関数において、ベースケース(終了条件)がない、または問題の規模が縮小されない場合、最大再帰深度の超過(RecursionError)を招きます。
# 再帰的関数:階乗(factorial)
def factorial(n):
    if n == 1:               
        return 1
    return n * factorial(n - 1)

6. ファイル操作・例外・モジュール (Files, Exceptions & Modules)

  • ファイルを開く際、encoding='utf-8' の指定を怠ると、Windowsなどの環境でテキストの文字化けや読み書きエラーの要因となります。
from pathlib import Path
path = Path('test-file.txt')
path.write_text('first line\nsecond line\n', encoding='utf-8')
  • readlines() を使ってファイルを行ごとに読み込むと、各行の末尾に改行文字 \n が付着したままになります。
# 基本的な with open の書き方
with open('test-file.txt', 'r', encoding='utf-8') as file_obj:
    lines = file_obj.readlines()    # 各行には通常、末尾の \n が付いている

lines                        # ['first line\n', 'second line\n']
  • 例外クラスを指定しない単なる except: (bare except)はキャッチする範囲が広すぎるため、システム終了など本当にキャッチすべきでないエラーまで握り潰して(swallow exceptions)しまいます。
# 複数処理を含む例外対応
def parse_age(text):
    try:
        age = int(text)
    except ValueError as exc: # ValueError を具体的に指定する
        return f'bad input: {exc}'
  • try ブロックは可能な限り小さく保ちます。範囲が広すぎるとどの行がエラートリガーになったかわかりづらくなります。
# try ブロックは極力狭める
def safe_divide(a, b):
    if b == 0:
        return None          # 業務上許容されるなら、事前に処理することも可能
    return a / b
  • 相対パスの起点は「スクリプトファイルが存在するディレクトリ」ではなく、「現在コードを実行した作業ディレクトリ(カレントディレクトリ/CWD)」です。
# 実行環境(カレントディレクトリ)に依存しないよう、
# このファイルからの相対的な絶対パス(プロジェクトルート)を取得する
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
  • finally ブロックは関数の returnbreak、例外の発生にかかわらず確実に実行されます。
def check_file():
    try:
        return True # たとえここで実行を終了しても...
    finally:
        pass # このエリアは常に実行される。主にリソースのクリーンアップに利用

7. クラスとオブジェクト指向 (Class & OOP)

  • インスタンスメソッドの定義時、最初の引数として self を記述し忘れるエラーが多発します。
class Golem:
    def __init__(self, name):
        self.name = name     # インスタンス属性(instance attribute)
  • カスタムクラス内で「インスタンス変数」であるべきものを「クラス変数」として変更してしまう落とし穴があります。
# クラス属性に可変オブジェクトを置く際の落とし穴
class BadBag:
    items = []               # すべてのインスタンスがこの1つのリストを共有する

    def add(self, item):
        self.items.append(item)

bag1 = BadBag()
bag2 = BadBag()
bag1.add('python')
bag2.items                   # ['python'] -> bag2 からも見えてしまう
  • クラス属性としてリストや辞書(可変オブジェクト)を定義すると、すべてのインスタンスで同一のオブジェクトが共有されてしまいます。対策はインスタンスが生成されるたびに新しいリストを作成することです。
class GoodBag:
    def __init__(self):
        # self.items は各インスタンス固有の変数(インスタンス変数)
        # インスタンスごとに独立した空リストが割り当てられる
        self.items = [] 
  • ごく単純なデータの処理であれば、無理にクラスを用いたオブジェクト指向(OOP)設計を取り入れる必要はありません。
# dataclass: 純粋なデータオブジェクトに適している
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p = Point(3, 5)
p                            # Point(x=3, y=5)

8. イテレータ・ジェネレータ・デコレータ・正規表現 (Iterators, Generators, Decorators & Regex)

  • イテレータは一度消費すると元に戻らない。
# イテレータ(iterator)
it = iter([10, 20, 30])
next(it)                     # 10
next(it)                     # 20

print(list(it))  # [30](10, 20 はもう取り出せない)
  • Generator(ジェネレータ)オブジェクトは通常1回しか反復実行(消費)できず、2回目のループでは空になります。
# ジェネレータ式(generator expression)
gen = (x * x for x in range(4))
list(gen)                    # [0, 1, 4, 9]
list(gen)                    # [] -> すでに消費されて空になっている
  • yield を使用した関数は、実行直後にリストを返すわけではなく、Generatorオブジェクトを生成して返します(遅延評価:lazy evaluation)。
# ジェネレータ関数(generator function)
def counter(start, stop):
    while start <= stop:
        yield start          # yield によって関数を一時停止し、値を一つ生み出す
        start += 1
list(counter(3, 5))          # [3, 4, 5]
  • デコレータを自作する際、元の関数の引数を受け継ぐための *args, **kwargs を怠ると、汎用性が失われます。
def trace(func):
    # 引数を固定してしまっている
    def wrapper(x, y):
        print("calling", func.__name__)
        return func(x, y)
    return wrapper


@trace
def add(a, b):
    return a + b

@trace
def greet(name):
    return f"Hello {name}"


print(add(2, 3))
print(greet("Bob"))
#calling add
#5
#trace.<locals>.wrapper() missing 1 required positional argument: 'y'
  • デコレータ関数内で @functools.wraps を使い忘れると、元の関数の名前やDocstring(メタデータ)が上書きされ失われます。
from functools import wraps

# シンプルなデコレータ
def trace(func):
    def wrapper(*args, **kwargs):
        print("calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper


@trace
def add(x, y):
    """加算する関数"""
    return x + y


print(add.__name__)   # wrapper
print(add.__doc__)    # None
  • 正規表現(Regex)の *+ はデフォルトで「貪欲(Greedy)マッチング」となります。最短一致をおこなう場合は .*? のように書きます。
import re
# 貪欲(greedy)と非貪欲(non-greedy)
html = '<b>one</b><b>two</b>'
re.findall(r'<b>.*</b>', html)
# ['<b>one</b><b>two</b>'] -> 貪欲マッチ

re.findall(r'<b>.*?</b>', html)
# ['<b>one</b>', '<b>two</b>'] -> 非貪欲マッチ(non-greedy)
  • re.match() は文字列の「先頭」からのみ合致を探しますが、文中のどの位置でも探す場合は re.search() を使用します。
# match vs search
re.match(r'world', 'hello world')   # None -> 先頭からのマッチではないため
re.search(r'world', 'hello world')  # マッチ成功
  • バックスラッシュの意図せぬエスケープを防ぐため、正規表現のパターンはRaw文字列(r'...')で記述することを強く推奨します。
# Raw 文字列(raw string):ファイルパスや正規表現に多用される
path = r'C:\Users\demo\notes.txt'
path                         # バックスラッシュがエスケープシーケンス(escape sequence)として解釈されない

9. 標準ライブラリと実用パターン (Standard Library & Practical pattern)

  • collections.Counter は、キーが存在しなかった場合に KeyError ではなく 0 を返します(通常の辞書との挙動の差異)。
from collections import Counter
# Counter:頻度のカウント
words = 'data science data python python python'.split()
word_counts = Counter(words)
word_counts['missing']         # 0 -> 存在しない key でもエラーにならない
  • collections.defaultdict は、存在しないキーに単にアクセス(参照)しただけでも、そのキーをデフォルト値付きで自動生成してしまいます。
from collections import defaultdict
# defaultdict:グループ化
groups = defaultdict(list)
for city, name in [('BJ', 'Ann'), ('BJ', 'Bob'), ('SH', 'Cindy')]:
    groups[city].append(name)  # key が存在するか事前に確認する必要がない
  • リストの先頭から要素を取り出す list.pop(0) は $O(n)$ の計算コストがかかります。左端からの取り出しが頻発する場合は collections.deque.popleft()($O(1)$)を使用します。
from collections import deque
# deque:両端キュー。BFS やキュー(queue)操作に適している
queue = deque([1, 2, 3])
queue.popleft()                # 左側から取得 -> 1
  • bisect による二分探索モジュールも、前提として「すでにソートされたリスト」に対してのみ有効です。
from bisect import bisect_left
# bisect:順序付きリストから挿入位置を探す
sorted_nums = [1, 3, 5, 7, 9]
idx = bisect_left(sorted_nums, 4)
idx                            # 2 -> 4 はインデックス 2 の位置に挿入されるべき
  • heapq はデフォルトで「最小ヒープ(Top要素が最小)」として動作します。最大ヒープとしてはそのまま使えません。
import heapq
# heapq:デフォルトで最小ヒープ(min-heap)
heap = [5, 1, 3, 9]
heapq.heapify(heap)
heapq.heappop(heap)            # 1 -> 毎回最小値を取り出す
  • datetime モジュールの strptime フォーマットにおいて、%m(月)と %M(分)など、大文字と小文字の取り違えに注意が必要です。
from datetime import datetime
# datetime:時間のパース、時間差の計算
dt = datetime.strptime('2026-04-03 09:30', '%Y-%m-%d %H:%M')
  • timedelta.days は日数の整数部のみを返します。秒などを含めた厳密な時間差が欲しい場合は total_seconds() を使用します。
from datetime import timedelta
# 総秒数の取得
total_sec = timedelta(days=1, hours=1).total_seconds()
  • キャッシュ用途で @functools.lru_cache を使う場合、その関数の引数に含まれるすべての値がハッシュ可能(hashable)である必要があります(リストなどは渡せません)。
from functools import lru_cache
# lru_cache:再帰関数のメモ化(memoization)
@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)
0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?