概要(「1ファイル地獄」から抜け出して、“小さな部品の集まり”にする)
Python アプリ構造化というのは、
「全部 main.py に書きなぐる」のをやめて、
「役割ごとにファイルやフォルダを分けて、部品として組み立てる」
ことです。
自動化スクリプトが育ってくると、
- 関数が増えすぎて、どこに何があるか分からない
- ログ、設定ファイル、CLI、処理本体がごちゃ混ぜ
- ちょっと直すだけで他のところが壊れそうで怖い
みたいな「1ファイル地獄」になりがちです。
構造化のゴールは、「機能を増やしても怖くない状態」にすることです。
そのために、
- ファイル・フォルダの分け方
- 役割ごとのコードの置き場所
main.pyに何を書いて、何を書かないか
を意識して設計していきます。
まずイメージを作る(小さな自動化アプリの全体像)
典型的な「自動化アプリ」の部品
例えば「API からデータを取って、CSV で保存する自動化ツール」を考えます。
それをちゃんと構造化すると、こんな部品に分けられます。
設定を読む部分(config)
コマンドライン引数を処理する部分(cli / argparse)
API を呼ぶ部分(services)
ファイル・ディレクトリを扱う部分(infrastructure)
ログ・エラーを扱う部分(logging)
全体の流れを組み立てる入口(main)
最初は全部1ファイルに書いてもいいですが、
機能が増えたら「役割ごとにモジュールに分ける」方向に育てていきます。
ここから、具体的なディレクトリ構造とコード例を見ながら説明します。
最小構成の「ちゃんとしたアプリ構造」を作る
ステップ1:1ファイルスクリプトの例(構造化前)
まず、よくある「全部入り」パターンから。
# bad_app.py
import requests
import csv
from pathlib import Path
def main():
input_url = "https://api.example.com/items"
output_path = Path("data/output.csv")
output_path.parent.mkdir(exist_ok=True)
resp = requests.get(input_url, timeout=5)
resp.raise_for_status()
items = resp.json()
with output_path.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=items[0].keys())
writer.writeheader()
writer.writerows(items)
if __name__ == "__main__":
main()
YAML最初はこれでいいです。
でも「URLを変えたい」「保存場所を環境ごとに変えたい」「ログを出したい」「CLI から日付を指定したい」と膨らんだ瞬間に、
このファイルがどんどん肥大化します。
これを少しずつ「部品化」していきます。
ステップ2:フォルダを切って、役割ごとにファイルを作る
次のような構造を目指してみます。
my_app/
main.py # 入口。全体の流れだけを書く
config.py # 設定読み込み(YAML など)
cli.py # argparse で引数を処理
services.py # ビジネスロジック(API 取得など)
io_utils.py # ファイル入出力・ディレクトリ関連
logging_conf.py # ログ設定
config.yaml # 設定ファイル
この段階で大事なのは、「ファイル名で役割が分かる」ことです。
中身を開かなくても、「ああ、ここにそれが書いてあるんだな」と推測できる状態にします。
各ファイルの役割を例付きで深掘りする
config.py(設定ファイルを読む係)
YAML でも JSON でもいいですが、「設定を読む場所」を1か所にまとめます。
# config.py
import yaml
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
CONFIG_PATH = BASE_DIR / "config.yaml"
def load_config(path: Path = CONFIG_PATH) -> dict:
with path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f)
YAMLconfig.yaml は例えばこう。
api:
base_url: "https://api.example.com"
timeout: 5.0
paths:
output_dir: "data"
YAMLポイントは、
Python 側は「キー構造」だけ知っていて、値そのものは YAML で変えられる
設定の読み方を変えたいとき(YAML → JSON など)は config.py だけ触ればいい
ということです。
cli.py(コマンドライン引数を扱う係)
argparse を使って、引数処理をまとめます。
# cli.py
import argparse
from datetime import date, timedelta
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="API からデータを取得して保存するツール"
)
parser.add_argument(
"--date",
help="取得対象日(YYYY-MM-DD)。省略時は昨日。",
required=False
)
return parser
def parse_args():
parser = build_parser()
return parser.parse_args()
def resolve_target_date(date_str: str | None) -> date:
if date_str:
return date.fromisoformat(date_str)
return date.today() - timedelta(days=1)
Pythonここで重要なのは、
引数パースのロジックを main.py から追い出している
日付文字列 → date 型への変換も cli 側でやっている
ことです。
main.py は「もう date 型として受け取れる」ので、
中身の処理がシンプルになります。
services.py(ビジネスロジック・API 呼び出し係)
ここには「アプリの目的となる処理」を書きます。
自動化なら「API からデータ取得」「フィルタリング」「集計」などです。
# services.py
import requests
def fetch_items(base_url: str, target_date: str, timeout: float = 5.0) -> list[dict]:
url = f"{base_url}/items"
params = {"date": target_date}
resp = requests.get(url, params=params, timeout=timeout)
resp.raise_for_status()
return resp.json()
Pythonここでは、
HTTP という手段は使っているが、
「引数を受け取って結果を返す」関数として設計されている
というのがポイントです。
この関数は CLI を知らないし、設定ファイルも知らないし、ログも知らない。
ただ「仕事」をして結果を返すだけの部品です。
io_utils.py(ファイル保存やディレクトリ操作係)
I/O 周りもまとめておきます。
# io_utils.py
from pathlib import Path
import csv
def ensure_dir(path: Path):
path.mkdir(parents=True, exist_ok=True)
def save_items_csv(items: list[dict], output_path: Path):
if not items:
return
ensure_dir(output_path.parent)
with output_path.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=items[0].keys())
writer.writeheader()
writer.writerows(items)
Pythonここも「CLI や設定を知らない」純粋な I/O 部品です。
Path と items さえ渡せば、仕事をしてくれます。
logging_conf.py(ログの初期化係)
ログも何度も書きたくないので、1か所にまとめます。
# logging_conf.py
import logging
from pathlib import Path
def setup_logging(log_file: Path):
log_file.parent.mkdir(exist_ok=True)
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)
Pythonこうしておけば、main.py から
- ログファイルのパスだけ決める
setup_loggingを一度呼ぶ
だけで済みます。
main.py には「流れ」だけを書く(ここが一番大事)
最終形の main.py のイメージ
ここまで作った部品を組み立てる入口が main.py です。
# main.py
import logging
from pathlib import Path
from config import load_config, BASE_DIR
from cli import parse_args, resolve_target_date
from services import fetch_items
from io_utils import save_items_csv
from logging_conf import setup_logging
def main():
config = load_config()
args = parse_args()
target_date = resolve_target_date(args.date)
api_cfg = config["api"]
paths_cfg = config["paths"]
output_dir = BASE_DIR / paths_cfg["output_dir"]
log_file = BASE_DIR / "logs" / "app.log"
setup_logging(log_file)
logging.info(f"処理開始 date={target_date}")
items = fetch_items(
base_url=api_cfg["base_url"],
target_date=target_date.isoformat(),
timeout=api_cfg.get("timeout", 5.0),
)
logging.info(f"取得件数: {len(items)}")
output_path = output_dir / f"items_{target_date}.csv"
save_items_csv(items, output_path)
logging.info(f"保存完了: {output_path}")
if __name__ == "__main__":
main()
Pythonここまで来ると、main.py の役割がすごくはっきりします。
設定を読む(config)
引数を読む(cli)
ログを初期化する(logging_conf)
サービス関数を呼ぶ(services)
I/O 関数を呼ぶ(io_utils)
つまり、「部品と部品を繋ぐだけ」のファイルですが、
それこそが main.py の理想形です。
なぜこれが強いのか(構造化のメリット)
この構造にしておくと、
API の仕様が変わったとき
→ services.py だけ触ればいい
保存形式を CSV から Excel に変えたいとき
→ io_utils.py だけ変えればいい
設定ファイル形式を YAML → JSON に変えたいとき
→ config.py だけ直せばいい
という風に、「変更の影響範囲」が小さくなります。
これが構造化の一番のメリットです。
初心者がハマりやすいポイントと意識してほしいこと
「モジュール間の依存関係」をできるだけ一方向にする
悪い例は、こういう状態です。
main が services を import
services が config を import
config が また services を import
みたいに、「モジュール同士が相互に import し合っている状態」です。
これは循環 import になってエラーの原因にもなるし、何より頭がこんがらがります。
基本の方針としては、
config は誰からも import される“根っこ”
cli も誰からも import される(逆方向には依存しない)
services と io_utils は、config や cli を知らない(引数だけ受け取る)
main だけが「全部を知っている司令塔」
という一方向の依存関係を意識してください。
「とりあえず関数に分ける」から始めて、あとでファイルを分ける
いきなり完璧な構造にしようとする必要はありません。
最初のステップとして、
1ファイルの中で、役割ごとに関数に分ける
→ 後から、関数のグループを別ファイル(モジュール)に移す
という順番で十分です。
例えば、
API 関連の関数が3つほど揃ってきた
→ それをまとめて services.py に移動する
という感じです。
構造化は「最初から完璧にやること」ではなく、
スクリプトが育つのに合わせて「整理し直していくこと」です。
まとめ(構造化は「壊れにくい自動化」を育てる作法)
Python アプリ構造化を、自動化の視点でまとめるとこうなります。
最初は1ファイルで書いてしまっていいが、機能が増えたら「役割ごとにモジュール(ファイル)に分ける」意識を持つ。
設定読み込み(config)、引数処理(cli)、ビジネスロジック(services)、I/O(io_utils)、ログ設定(logging_conf)、入口(main)といった“部品”に分解していく。main.py は「部品と部品をつなぐだけ」にして、ロジックは関数として別モジュールに追い出していく。
モジュール同士の依存関係を一方向に保つことで、変更の影響範囲を小さくできる。
構造化は一気にやるものではなく、「まず関数に分ける → 役割ごとにファイルを分ける」と段階的に育てていく。
ここまでの考え方がつかめていれば、
今あなたが持っている自動化スクリプトを、
どこからどう分割していくと楽になるか、一緒に具体例まで落とし込めます。

