2.3.5 ハンズオンワークショップ:ローカル学習タスクアシスタントを作る

何を作るのか
Section titled “何を作るのか”learning_assistant_cli.py というコマンドライン学習タスクアシスタントを作ります。Python 標準ライブラリだけを使うため、サードパーティパッケージをインストールする必要はありません。
手順どおりに進めると、次のようなコマンドを実行できるようになります。
python3 learning_assistant_cli.py seedpython3 learning_assistant_cli.py listpython3 learning_assistant_cli.py add "Practice command-line arguments" --stage 2.3 --tag argparsepython3 learning_assistant_cli.py done 2python3 learning_assistant_cli.py statspython3 learning_assistant_cli.py exportプロジェクトは次のファイルを作ります。
| ファイル | 目的 |
|---|---|
learning_assistant_cli.py | 実行できる Python プログラム |
ch02_output/tasks.json | 保存された学習タスク |
ch02_output/learning_report.md | ポートフォリオ証拠として使える出力レポート |
ステップ 0:きれいな練習フォルダを作る
Section titled “ステップ 0:きれいな練習フォルダを作る”ターミナルで実行します。
mkdir ch02-learning-assistant-workshopcd ch02-learning-assistant-workshoppython3 --version出力は次のようになります。バージョン番号は違っていてかまいません。
Python 3.12.3このワークショップでは dataclass、list[str]、str | None などの現代的な Python 標準ライブラリ構文を使います。Python 3.10 以降を使ってください。
ステップ 1:まずプログラム全体の流れを見る
Section titled “ステップ 1:まずプログラム全体の流れを見る”
プログラムは単純な流れで動きます。
| 手順 | 何が起きるか | 対応する Python 概念 |
|---|---|---|
| ユーザーがコマンドを入力 | add、list、done、stats、export のどれか | コマンドライン引数 |
argparse が解析 | コマンドが構造化された値になる | 関数とモジュール |
| プログラムが JSON を読む | 保存済みタスクをディスクから読む | ファイル入出力と例外 |
| コマンド関数が動く | データを変更または集計する | リスト、辞書、ループ |
| プログラムが出力を保存 | JSON または Markdown を書き戻す | 永続化 |
コードを読むときは、この図を頭に置いてください。作っているのは孤立した構文練習ではなく、小さいけれど完成したプログラムです。
ステップ 2:完全なスクリプトを作る
Section titled “ステップ 2:完全なスクリプトを作る”learning_assistant_cli.py というファイルを作り、次のコードを貼り付けます。
from __future__ import annotations
import argparseimport jsonfrom dataclasses import asdict, dataclass, fieldfrom datetime import datetime, timezonefrom pathlib import Path
OUTPUT_DIR = Path("ch02_output")DATA_FILE = OUTPUT_DIR / "tasks.json"REPORT_FILE = OUTPUT_DIR / "learning_report.md"
def utc_now() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
@dataclassclass Task: id: int title: str stage: str tags: list[str] done: bool = False created_at: str = field(default_factory=utc_now) completed_at: str | None = None
def ensure_output_dir() -> None: OUTPUT_DIR.mkdir(exist_ok=True)
def load_tasks() -> list[Task]: if not DATA_FILE.exists(): return [] try: raw_tasks = json.loads(DATA_FILE.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise SystemExit(f"Cannot read {DATA_FILE}: invalid JSON at line {exc.lineno}. Fix or remove the file, then rerun.") from exc return [Task(**item) for item in raw_tasks]
def save_tasks(tasks: list[Task]) -> None: ensure_output_dir() data = [asdict(task) for task in tasks] DATA_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def next_id(tasks: list[Task]) -> int: if not tasks: return 1 return max(task.id for task in tasks) + 1
def seed_tasks(_: argparse.Namespace) -> None: tasks = [ Task(id=1, title="Read Python functions", stage="2.1", tags=["functions"]), Task(id=2, title="Practice JSON file saving", stage="2.2", tags=["json", "file-io"]), Task(id=3, title="Build the first CLI command", stage="2.3", tags=["cli"]), ] save_tasks(tasks) print(f"Wrote {len(tasks)} sample tasks to {DATA_FILE}")
def add_task(args: argparse.Namespace) -> None: title = args.title.strip() if not title: raise SystemExit("Task title cannot be empty.") tasks = load_tasks() task = Task(id=next_id(tasks), title=title, stage=args.stage, tags=args.tag) tasks.append(task) save_tasks(tasks) print(f"Added task #{task.id}: {task.title}")
def list_tasks(_: argparse.Namespace) -> None: tasks = load_tasks() if not tasks: print("No tasks yet. Run: python learning_assistant_cli.py add \"Read functions\"") return print("ID Status Stage Title") print("-- ------ ----- -----") for task in tasks: status = "done" if task.done else "todo" print(f"{task.id:<2} {status:<6} {task.stage:<5} {task.title}")
def complete_task(args: argparse.Namespace) -> None: tasks = load_tasks() for task in tasks: if task.id == args.id: task.done = True task.completed_at = utc_now() save_tasks(tasks) print(f"Completed task #{task.id}: {task.title}") return raise SystemExit(f"Task #{args.id} was not found.")
def show_stats(_: argparse.Namespace) -> None: tasks = load_tasks() total = len(tasks) done = sum(task.done for task in tasks) todo = total - done by_stage: dict[str, int] = {} for task in tasks: by_stage[task.stage] = by_stage.get(task.stage, 0) + 1 rate = (done / total * 100) if total else 0 print(f"Total tasks: {total}") print(f"Done: {done}") print(f"Todo: {todo}") print(f"Completion rate: {rate:.1f}%") print("Tasks by stage:") for stage, count in sorted(by_stage.items()): print(f"- {stage}: {count}")
def export_report(_: argparse.Namespace) -> None: tasks = load_tasks() done = sum(task.done for task in tasks) total = len(tasks) lines = [ "# Python Learning Assistant Report", "", f"Generated at: {utc_now()}", f"Total tasks: {total}", f"Completed tasks: {done}", "", "## Tasks", "", ] for task in tasks: checkbox = "x" if task.done else " " tags = ", ".join(task.tags) if task.tags else "-" lines.append(f"- [{checkbox}] #{task.id} {task.title} (stage {task.stage}; tags: {tags})") ensure_output_dir() REPORT_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"Exported report to {REPORT_FILE}")
def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Local learning-task assistant for Chapter 2 Python practice.") subparsers = parser.add_subparsers(dest="command", required=True)
seed_parser = subparsers.add_parser("seed", help="Create sample tasks.") seed_parser.set_defaults(func=seed_tasks)
add_parser = subparsers.add_parser("add", help="Add one learning task.") add_parser.add_argument("title", help="Task title, wrapped in quotes if it contains spaces.") add_parser.add_argument("--stage", default="2.1", help="Course stage or section, such as 2.1 or 2.3.") add_parser.add_argument("--tag", action="append", default=[], help="Repeatable tag, such as --tag functions --tag json.") add_parser.set_defaults(func=add_task)
list_parser = subparsers.add_parser("list", help="List tasks.") list_parser.set_defaults(func=list_tasks)
done_parser = subparsers.add_parser("done", help="Mark one task as complete.") done_parser.add_argument("id", type=int, help="Task id to complete.") done_parser.set_defaults(func=complete_task)
stats_parser = subparsers.add_parser("stats", help="Show task statistics.") stats_parser.set_defaults(func=show_stats)
export_parser = subparsers.add_parser("export", help="Export a Markdown report.") export_parser.set_defaults(func=export_report) return parser
def main() -> None: parser = build_parser() args = parser.parse_args() args.func(args)
if __name__ == "__main__": main()ステップ 3:最初のコマンドを実行する
Section titled “ステップ 3:最初のコマンドを実行する”python3 learning_assistant_cli.py seed期待される出力:
Wrote 3 sample tasks to ch02_output/tasks.json次にタスクを一覧表示します。
python3 learning_assistant_cli.py list期待される出力:
ID Status Stage Title-- ------ ----- -----1 todo 2.1 Read Python functions2 todo 2.2 Practice JSON file saving3 todo 2.3 Build the first CLI commandステップ 4:タスクを追加し、完了にする
Section titled “ステップ 4:タスクを追加し、完了にする”
新しいタスクを追加します。
python3 learning_assistant_cli.py add "Practice command-line arguments" --stage 2.3 --tag argparse期待される出力:
Added task #4: Practice command-line argumentsタスク 2 を完了にします。
python3 learning_assistant_cli.py done 2期待される出力:
Completed task #2: Practice JSON file savingこの時点で ch02_output/tasks.json を開くと、通常の JSON データが見えるはずです。タイムスタンプは環境ごとに違いますが、タスク 2 の done フィールドは true になっているはずです。
ステップ 5:統計を表示し、レポートを書き出す
Section titled “ステップ 5:統計を表示し、レポートを書き出す”python3 learning_assistant_cli.py stats期待される出力:
Total tasks: 4Done: 1Todo: 3Completion rate: 25.0%Tasks by stage:- 2.1: 1- 2.2: 1- 2.3: 2Markdown レポートを書き出します。
python3 learning_assistant_cli.py export期待される出力:
Exported report to ch02_output/learning_report.mdこれで、実行できるプロジェクトと、ポートフォリオ証拠として使える小さなレポートができました。
ステップ 6:重要な部分を理解する
Section titled “ステップ 6:重要な部分を理解する”| コード部分 | 何を練習しているか | 後でなぜ重要か |
|---|---|---|
argparse | ターミナルのコマンドを構造化された値に変換する | CLI、スクリプト、自動化ツールには明確な入力が必要 |
@dataclass | タスクをフィールドで説明する | 後の API モデル、データベース行、設定オブジェクトと同じ考え方 |
load_tasks() | JSON を読み、壊れた JSON に対応する | 実際のプログラムは、存在しないファイルや壊れたファイルにも耐える必要がある |
save_tasks() | Python オブジェクトを JSON に変換する | 永続化の最小版 |
| コマンド関数 | 1 つのコマンドを 1 つの関数に分ける | 大きなプロジェクトは明確な関数境界に依存する |
export_report() | 内部データをユーザー向けの出力にする | AI ツールやデータツールでは、レポート、ログ、証拠がよく必要になる |
このページを終えたら、この evidence card を残します。
- プロジェクト目標
- CLI、スクレイパー、API、AI API 呼び出し、または統合 Python ワークショップの対象
- 実行コマンド
- プロジェクトの起動に使った正確なコマンド
- 成果物
- 出力ファイル、API 応答、JSON レコード、スクリーンショット、または README メモ
- 失敗確認
- 依存関係、ネットワーク、パース、ルート、入力検証、または API キーの問題
- 期待される成果
- 実行結果と1件の失敗例を含む再現可能なミニプロジェクトフォルダ
よくあるエラーと直し方
Section titled “よくあるエラーと直し方”
| 問題 | よくある原因 | 修正 |
|---|---|---|
python3: command not found | 環境では python を使う設定になっている | python --version を試し、python learning_assistant_cli.py seed を実行する |
Task #99 was not found. | 存在しないタスク id を完了にしようとしている | 先に python3 learning_assistant_cli.py list を実行する |
invalid JSON エラー | tasks.json を手動編集して形式を壊した | JSON ファイルを修正するか、削除してから seed を実行する |
| レポートが空 | まだタスクを作っていない | seed または add を実行してから export する |
| コードは読めるが変更できない | スクリプト全体を一度に見て大きく感じている | 1 回に 1 つのコマンドだけ変更し、そのコマンドだけ再実行する |
deleteコマンドを追加し、id でタスクを削除する。searchコマンドを追加し、キーワードを含むタスクを探す。listに--tagフィルタを追加する。export_report()を変更し、未完了タスクを先に出す。- わざと
tasks.jsonを壊し、listを実行して、エラーメッセージと修正方法を記録する。
操作例と確認ポイント
deleteは id を受け取り、tasks.jsonから該当アイテムを削除し、明確な確認メッセージを出します。もう一度listを実行して、行が本当に消えたことを確認します。searchはtitleをキーワードで絞り込み、必要ならtagsも対象にし、大文字小文字を区別せずに一致したものだけを表示します。--tagはlistのargparseフィルタにするのが最適です。保存データを変更せずにコマンドを再利用できます。- レポートで今やる作業を先に目立たせたいなら、
export_report()で未完了タスクを完了済みより前に並べます。形式を安定させておくと diff が読みやすくなります。 - わざと
tasks.jsonを壊してlistを実行し、スクリプトがクラッシュせずに分かりやすい JSON エラーを出すことを確認します。その後、ファイルを修復するか削除してからseedを再実行します。
ポートフォリオ用の証拠チェックリスト
Section titled “ポートフォリオ用の証拠チェックリスト”
証拠として次のファイルを残しましょう。
learning_assistant_cli.pych02_output/tasks.jsonch02_output/learning_report.mdseed、list、done、stats、exportを実行したスクリーンショット、またはコピーしたターミナル出力- ツールの実行方法と、対応したエラーを書いた短い
README.md
第 2 章の核心はこれです。構文で止まらず、実行でき、データを保存でき、エラーを扱え、説明できる小さなツールに変えましょう。