実務で役立つPythonのベストプラクティスを、コードスタイルからプロジェクト構成、テスト、パフォーマンスまでまとめたガイドです。
目次
- コードスタイル
- 命名規則
- 型ヒント
- 関数とクラスの設計
- Pythonicな書き方
- エラーハンドリング
- リソース管理
- プロジェクト構成
- 依存関係と仮想環境
- テスト
- ロギング
- パフォーマンス
- セキュリティ
- ドキュメント
- ツールチェーンのおすすめ構成
- よくあるアンチパターン
1. コードスタイル
PEP 8 に従う
- インデントはスペース4つ
- 1行の長さはプロジェクトで統一(PEP 8 は79文字、実務では88〜100文字が一般的)
- トップレベルの関数・クラスの間は2行、メソッド間は1行空ける
フォーマッタとリンタに任せる
スタイルの議論に時間を使わず、ツールで自動化します。
ruff format . # フォーマット
ruff check . # リント
ruff check --fix . # 自動修正import の順序
- 標準ライブラリ
- サードパーティ
- 自プロジェクト
import os
from pathlib import Path
import requests
from pydantic import BaseModel
from myapp.config import settingsfrom module import *は使わない(名前空間が汚染され、どこから来た名前か分からなくなる)- 絶対importを基本とする
2. 命名規則
| 対象 | 規則 | 例 |
|---|---|---|
| 変数・関数 | snake_case | user_count, get_user() |
| クラス | PascalCase | UserRepository |
| 定数 | UPPER_SNAKE_CASE | MAX_RETRIES |
| モジュール | 短い snake_case | utils.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-format16. よくあるアンチパターン
| アンチパターン | 改善策 |
|---|---|
except: pass | 具体的な例外を捕捉し、ログを残す |
| ミュータブルなデフォルト引数 | None をデフォルトにして関数内で生成 |
range(len(x)) でのループ | enumerate() を使う |
type(x) == int | isinstance(x, int) を使う |
== None | is None を使う |
| グローバル変数の多用 | 引数・戻り値・クラスで状態を管理 |
from x import * | 必要な名前だけを明示的にimport |
ループ内での += による文字列連結 | "".join() を使う |
print デバッグの放置 | logging を使う |
| 巨大な関数・クラス | 単一責務に分割する |
| ハードコードされたパス・設定値 | 設定ファイル・環境変数・pathlib を使う |
open() を with なしで使う | with 文で確実にクローズ |