実務で役立つPythonのベストプラクティスを、コードスタイルからプロジェクト構成、テスト、パフォーマンスまでまとめたガイドです。


目次

  1. コードスタイル
  2. 命名規則
  3. 型ヒント
  4. 関数とクラスの設計
  5. Pythonicな書き方
  6. エラーハンドリング
  7. リソース管理
  8. プロジェクト構成
  9. 依存関係と仮想環境
  10. テスト
  11. ロギング
  12. パフォーマンス
  13. セキュリティ
  14. ドキュメント
  15. ツールチェーンのおすすめ構成
  16. よくあるアンチパターン

1. コードスタイル

PEP 8 に従う

  • インデントはスペース4つ
  • 1行の長さはプロジェクトで統一(PEP 8 は79文字、実務では88〜100文字が一般的)
  • トップレベルの関数・クラスの間は2行、メソッド間は1行空ける

フォーマッタとリンタに任せる

スタイルの議論に時間を使わず、ツールで自動化します。

ruff format .   # フォーマット
ruff check .    # リント
ruff check --fix .  # 自動修正

import の順序

  1. 標準ライブラリ
  2. サードパーティ
  3. 自プロジェクト
import os
from pathlib import Path
 
import requests
from pydantic import BaseModel
 
from myapp.config import settings
  • from module import * は使わない(名前空間が汚染され、どこから来た名前か分からなくなる)
  • 絶対importを基本とする

2. 命名規則

対象規則例
変数・関数snake_caseuser_count, get_user()
クラスPascalCaseUserRepository
定数UPPER_SNAKE_CASEMAX_RETRIES
モジュール短い snake_caseutils.py, user_service.py
内部用(非公開)先頭に __parse_header()
名前マングリング先頭に ____secret(多用しない)

ポイント

  • 意味のある名前をつける:d より elapsed_days
  • 真偽値は is_ / has_ / can_ で始める:is_active, has_permission
  • 組み込み名(list, id, type, input など)を変数名に使わない

3. 型ヒント

型ヒントは可読性・IDE補完・静的解析の精度を大きく向上させます。

def find_user(user_id: int, *, include_deleted: bool = False) -> User | None:
    ...

モダンな書き方を使う(Python 3.10+)

# ✅ 推奨
def process(items: list[str], mapping: dict[str, int] | None = None) -> tuple[int, ...]:
    ...
 
# ❌ 古い書き方
from typing import List, Dict, Optional, Tuple
def process(items: List[str], mapping: Optional[Dict[str, int]] = None) -> Tuple[int, ...]:
    ...

便利な型

from collections.abc import Callable, Iterable, Iterator, Sequence, Mapping
from typing import Literal, TypedDict, Protocol, Final
 
Mode = Literal["read", "write"]
MAX_SIZE: Final = 1024
 
class Closeable(Protocol):
    def close(self) -> None: ...
  • 引数は抽象型(Iterable, Sequence, Mapping)、戻り値は具体型(list, dict)を使うと柔軟になる
  • Any は最小限に。使う場合は理由をコメントで残す
  • 型チェッカー(mypy / pyright)をCIで実行する

4. 関数とクラスの設計

関数は小さく、1つのことだけを行う

# ❌ 何でもやる関数
def handle(data): ...
 
# ✅ 責務を分割
def validate(data: dict) -> None: ...
def transform(data: dict) -> Record: ...
def save(record: Record) -> None: ...

ミュータブルなデフォルト引数を使わない

# ❌ リストが呼び出し間で共有される
def add_item(item, items=[]):
    items.append(item)
    return items
 
# ✅
def add_item(item: str, items: list[str] | None = None) -> list[str]:
    if items is None:
        items = []
    items.append(item)
    return items

キーワード専用引数で誤用を防ぐ

def connect(host: str, port: int, *, timeout: float = 10.0, retries: int = 3) -> None:
    ...
 
connect("localhost", 5432, timeout=5.0)  # 意図が明確

データ保持には dataclass を使う

from dataclasses import dataclass, field
 
@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float
 
@dataclass
class Order:
    id: int
    items: list[str] = field(default_factory=list)
  • 不変にできるものは frozen=True
  • 外部入力のバリデーションが必要なら pydantic を検討

継承よりコンポジション

深い継承階層は避け、必要な機能を持つオブジェクトを組み合わせる。インターフェースの定義には Protocol や abc.ABC を使う。


5. Pythonicな書き方

内包表記

squares = [x * x for x in range(10) if x % 2 == 0]
name_by_id = {u.id: u.name for u in users}

ただし複雑になりすぎたら普通のループに戻す。

ジェネレータで大きなデータを扱う

total = sum(x * x for x in range(10_000_000))  # リストを作らない
 
def read_lines(path: Path) -> Iterator[str]:
    with path.open() as f:
        for line in f:
            yield line.rstrip("\n")

enumerate と zip

# ❌
for i in range(len(items)):
    print(i, items[i])
 
# ✅
for i, item in enumerate(items):
    print(i, item)
 
for name, score in zip(names, scores, strict=True):  # 長さ不一致を検出
    ...

アンパック

first, *rest = values
a, b = b, a  # スワップ

真偽値の判定

if items:          # ✅  if len(items) > 0: ではなく
if value is None:  # ✅  == None ではなく
if flag:           # ✅  == True ではなく

f-string を使う

msg = f"{user.name} has {count:,} points ({ratio:.1%})"
print(f"{value=}")  # デバッグ用: value=42 と出力

pathlib でパス操作

from pathlib import Path
 
config_path = Path(__file__).parent / "config" / "settings.toml"
text = config_path.read_text(encoding="utf-8")

構造的パターンマッチ(3.10+)

match command:
    case {"action": "move", "x": x, "y": y}:
        move(x, y)
    case {"action": "quit"}:
        quit()
    case _:
        raise ValueError(f"Unknown command: {command}")

EAFP(許可より謝罪)

# LBYL(Look Before You Leap)
if key in mapping:
    value = mapping[key]
 
# EAFP — Pythonらしい書き方
try:
    value = mapping[key]
except KeyError:
    value = default
 
# もちろん、この例なら単に
value = mapping.get(key, default)

6. エラーハンドリング

具体的な例外を捕捉する

# ❌ すべてを握りつぶす
try:
    do_something()
except:
    pass
 
# ✅
try:
    data = json.loads(raw)
except json.JSONDecodeError as e:
    logger.warning("Invalid JSON: %s", e)
    raise
  • 素の except: や except Exception: pass は避ける
  • try ブロックは最小範囲にする

例外チェーンを保つ

try:
    conn = connect(url)
except OSError as e:
    raise DatabaseError(f"Failed to connect to {url}") from e

独自例外を階層化する

class AppError(Exception):
    """アプリケーションの基底例外"""
 
class ValidationError(AppError): ...
class NotFoundError(AppError): ...

呼び出し側は except AppError でまとめて捕捉できる。

else と finally

try:
    result = compute()
except ValueError:
    handle_error()
else:
    save(result)       # 例外が起きなかった時だけ
finally:
    cleanup()          # 必ず実行

7. リソース管理

ファイル・ロック・接続などは必ず with 文で扱う。

with open("data.txt", encoding="utf-8") as f:
    content = f.read()
  • テキストファイルは常に encoding を明示する(環境依存を避ける)

独自のコンテキストマネージャ

from contextlib import contextmanager
import time
 
@contextmanager
def timer(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.3f}s")
 
with timer("heavy task"):
    heavy_task()

8. プロジェクト構成

src レイアウトを推奨

my-project/
├── pyproject.toml
├── README.md
├── src/
│   └── my_package/
│       ├── __init__.py
│       ├── __main__.py
│       ├── core.py
│       └── utils.py
└── tests/
    ├── conftest.py
    └── test_core.py
  • src/ 配下に置くことで、インストールせずにテストが通ってしまう事故を防げる
  • 設定は pyproject.toml に集約する(setup.py / setup.cfg は新規では不要)

pyproject.toml の例

[project]
name = "my-package"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "requests>=2.31",
]
 
[dependency-groups]
dev = ["pytest", "ruff", "mypy"]
 
[tool.ruff]
line-length = 100
 
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM"]
 
[tool.mypy]
strict = true

エントリーポイント

def main() -> None:
    ...
 
if __name__ == "__main__":
    main()

モジュールのトップレベルで重い処理や副作用のある処理を書かない。


9. 依存関係と仮想環境

  • プロジェクトごとに仮想環境を分ける(システムのPythonに直接インストールしない)
  • ロックファイルで再現性を確保する

uv を使う場合(高速でおすすめ)

uv init my-project
uv add requests
uv add --dev pytest ruff
uv run pytest
uv sync          # ロックファイルから環境を再現

標準ツールのみの場合

python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

ポイント

  • .venv/ は .gitignore に入れる
  • ライブラリは依存のバージョンを緩めに、アプリケーションはロックファイルで厳密に固定
  • 依存は定期的に更新し、脆弱性をチェックする(pip-audit など)

10. テスト

pytest を使う

# tests/test_calc.py
import pytest
from my_package.calc import divide
 
def test_divide():
    assert divide(10, 2) == 5
 
def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)
 
@pytest.mark.parametrize(("a", "b", "expected"), [
    (6, 3, 2),
    (9, 3, 3),
    (-4, 2, -2),
])
def test_divide_parametrized(a, b, expected):
    assert divide(a, b) == expected

フィクスチャで準備処理を共通化

@pytest.fixture
def user() -> User:
    return User(id=1, name="Alice")
 
def test_user_name(user: User):
    assert user.name == "Alice"

テストのコツ

  • 1テスト1関心事。テスト名で何を検証しているか分かるようにする
  • 外部依存(API・DB・時刻)はモックや差し替え可能な設計にする
  • カバレッジは pytest-cov で計測(数値より重要なロジックが網羅されているかを重視)
  • CIで毎回テスト・リント・型チェックを実行する

11. ロギング

print ではなく logging を使う。

import logging
 
logger = logging.getLogger(__name__)
 
def process(order_id: int) -> None:
    logger.info("Processing order %s", order_id)  # 遅延フォーマット
    try:
        ...
    except Exception:
        logger.exception("Failed to process order %s", order_id)  # スタックトレース付き
        raise
  • モジュールごとに logging.getLogger(__name__)
  • ログ設定(basicConfig など)はアプリケーションのエントリーポイントで1回だけ
  • ライブラリではハンドラを設定しない(利用側に任せる)
  • メッセージは %s プレースホルダで渡す(f-stringは出力されなくても評価される)
  • パスワードやトークンをログに出さない

12. パフォーマンス

まず計測する

python -m cProfile -s cumtime script.py
python -m timeit "sum(range(1000))"

推測で最適化しない。ボトルネックを特定してから手を入れる。

適切なデータ構造を選ぶ

用途推奨
存在チェックset / dict(O(1))※ list は O(n)
両端の追加・削除collections.deque
カウントcollections.Counter
デフォルト値付き辞書collections.defaultdict
優先度付きキューheapq

その他のテクニック

# 文字列の連結は join
result = "".join(parts)          # ✅
# result += part をループで繰り返さない
 
# 結果のキャッシュ
from functools import cache
 
@cache
def fib(n: int) -> int:
    return n if n < 2 else fib(n - 1) + fib(n - 2)
  • 数値計算は NumPy / pandas / Polars などのベクトル化を活用
  • I/O待ちが多い処理は asyncio やスレッド、CPUバウンドな処理は multiprocessing / concurrent.futures.ProcessPoolExecutor

13. セキュリティ

  • 秘密情報をコードに書かない:環境変数やシークレット管理サービスを使う

  • eval() / exec() を信頼できない入力に使わない

  • pickle で信頼できないデータを読み込まない(任意コード実行の危険)

  • SQLはプレースホルダを使う

    # ❌ SQLインジェクション
    cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
    # ✅
    cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
  • subprocess では shell=True を避け、引数はリストで渡す

    subprocess.run(["ls", "-l", path], check=True)
  • YAML は yaml.safe_load を使う

  • 乱数でトークンを作るなら secrets モジュール(random は暗号用途に不適)

  • 依存パッケージの脆弱性を定期的にチェックする


14. ドキュメント

docstring を書く

公開API(モジュール・クラス・関数)には docstring を書く。スタイル(Google / NumPy / reST)はプロジェクトで統一する。

def fetch_user(user_id: int, *, timeout: float = 5.0) -> User:
    """ユーザー情報をAPIから取得する。
 
    Args:
        user_id: 取得対象のユーザーID。
        timeout: タイムアウト秒数。
 
    Returns:
        取得したユーザー。
 
    Raises:
        NotFoundError: ユーザーが存在しない場合。
    """

コメントは「なぜ」を書く

# ❌ 何をしているか(コードを見れば分かる)
i += 1  # iを1増やす
 
# ✅ なぜそうしているか
# APIが1始まりのインデックスを要求するため
page = index + 1
  • READMEにはセットアップ方法・使い方・開発手順を書く

15. ツールチェーンのおすすめ構成

目的ツール
パッケージ・環境管理uv(または pip + venv、poetry)
フォーマット・リントruff
型チェックmypy / pyright
テストpytest, pytest-cov
コミット前チェックpre-commit
脆弱性チェックpip-audit

pre-commit の設定例

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.x.x  # 最新版を指定
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

16. よくあるアンチパターン

アンチパターン改善策
except: pass具体的な例外を捕捉し、ログを残す
ミュータブルなデフォルト引数None をデフォルトにして関数内で生成
range(len(x)) でのループenumerate() を使う
type(x) == intisinstance(x, int) を使う
== Noneis None を使う
グローバル変数の多用引数・戻り値・クラスで状態を管理
from x import *必要な名前だけを明示的にimport
ループ内での += による文字列連結"".join() を使う
print デバッグの放置logging を使う
巨大な関数・クラス単一責務に分割する
ハードコードされたパス・設定値設定ファイル・環境変数・pathlib を使う
open() を with なしで使うwith 文で確実にクローズ

参考

0件のページ