【Python】標準モジュールだけでスケジュール管理アプリ作ってみた

はじめに

こんにちは。

新しい現場に入ると「セキュリティの関係で外部ツールが使えない」「勝手にライブラリをインストールできない」という壁にぶつかること、ありますよね。

今回は、Pythonに標準で入っている sqlite3argparse だけを使って、自分専用のスケジュール管理アプリ(CLIツール)を作ります。

main関数の処理の順番に沿って、解説していきます。よろしくお願いします!

全体のコードへジャンプ

【環境】

OS: macOS (Apple M3 Chip)

言語: Python 3.14

ライブラリー: 標準ライブラリのみ(sqlite3, argparse, datetime)

解説

データベースに接続する

この関数はデータベースの管理をしています。

今回は In-Memory SQLite を活用します。

  • データ構造:
    • id: 各予定を識別するユニークなキー(整数)
    • title: 予定の名前(テキスト)
    • due_date: 期限(日付データ)
    • status: 「未完了/完了」などのステータス

ファイル作成の権限を気にせず、メモリ内でサクッとテーブルを作って管理できるのがこのツールの強みです。

def init_db():
with sqlite3.connect("schedule.db") as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS task(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
due_date TEXT NOT NULL,
status TEXT DEFAULT '未完了'
)
""")

parserを作る

次に、ターミナルで打ち込んだ文字を理解するための「解析リーダー(parser)」を呼び出します。

-h と打った時に説明を表示したり、入力ミスを教えてくれたりする司令塔です。

parser = argparse.ArgumentParser(description="ツール")

argparse.pyのArgumentParserクラスを呼び出します。

命令(サブコマンド)の分岐点を作る

subparsers = parser.add_subparsers(dest="command")

ここで、「追加するのか(add)」「表示するのか(list)」という道の分岐点を作ります。

ここでは、parsers.pyのadd_subparsersを呼び出しています。

argparse.py(標準ライブラリ)

🌸二重に作ろうとしていないかチェック

if self._subparsers is not None:
    raise ValueError('cannot have multiple subparser arguments')

🌸デフォルト値の設定

kwargs.setdefault('parser_class', type(self))

子分となるパーサー(add用やlist用)も、親分と同じ種類(クラス)で作るように設定しています。

🌸見た目を整える

if 'title' in kwargs or 'description' in kwargs:
title = kwargs.pop('title', _('subcommands'))
# ...
self._subparsers = self.add_argument_group(title, description)

もし「メニュー」にタイトル(例:命令一覧)や説明を付けたい場合は、それを綺麗にグループ化して表示する準備をします。今回はタイトルを指定していないので、else の方に流れて、標準的な位置(_positionals)に「棚」が設置されます。

🌸「プログラム名」の自動設定

ターミナルで python3 main.py -h と打った時に、一番上に usage: main.py ... と出ますが、プログラム名 がまだ決まっていない場合、ここで自動的に今のファイル名を取得して設定してくれています。ユーザーに「このプログラムはどうやって使うのか」を教えるための処理です。

if kwargs.get('prog') is None:
formatter = self._get_formatter()
# ... 中略 ...
kwargs['prog'] = formatter.format_help().strip()

🌸サブコマンド用のActionを作成

parsers_class = self._pop_action_class(kwargs, 'parsers')
action = parsers_class(option_strings=[], **kwargs)

サブコマンドとはaddやlistのことです。

これらの特定のキーワードが来たら、どういう処理をするのか決める、Actionを作成します。

🌸サブコマンドからコード全体へ

self._check_help(action)
self._subparsers._add_action(action)
return action

サブコマンドの情報をプログラム全体に登録しています。

これによって、main.py add ~のように書いた時に、正しく処理が引き継がれるようになります。

サブコマンド(add)のパーサーを作る

main.py

add_parser = subparsers.add_parser("add",help="予定を追加")

特定のサブコマンド(addまたはlist)のパーサーを作ります。

argparse.py(標準ライブラリ)

1.実行プログラム名(prog)の生成

        if kwargs.get('prog') is None:
kwargs['prog'] = '%s %s' % (self._prog_prefix, name)

子パーサーが持つ「プログラム名」を決定します。親の接頭辞(例: main.py)と、

今回追加するコマンド名(例: add)を連結して todo.py add という文字列を作り、

設定(kwargs)に格納します。

2. 重複チェック

        if name in self._name_parser_map:
raise ValueError(f'conflicting subparser: {name}')
for alias in aliases:
if alias in self._name_parser_map:
raise ValueError(f'conflicting subparser alias: {alias}')

すでに同じ名前のサブコマンドが登録されていないか確認します。

_name_parser_map という辞書をチェックし、存在すればエラーを投げます。

別名(aliases)についても同様のループ処理で重複を確認します。

3. ヘルプ情報の分離と保持

        if 'help' in kwargs:
help = kwargs.pop('help')
choice_action = self._ChoicesPseudoAction(name, aliases, help)
self._choices_actions.append(choice_action)
else:
choice_action = None

引数で渡された help 文字列(「予定を追加」など)を取り出し、

専用のオブジェクト(PseudoAction)に変換します。

これは解析自体には使いませんが、ヘルプ画面を表示する際の一覧データとして _choices_actions リストに保存されます。

4. 子パーサー・オブジェクトのインスタンス化

parser = self._parser_class(**kwargs)

実際にArgumentParserを作成します。

ここで、親と同じクラスのインスタンスが生成されます。

これが main.py 側で受け取る add_parser の実体です。

5. マッピング(地図)への登録

        if choice_action is not None:
parser._check_help(choice_action)
self._name_parser_map[name] = parser

# make parser available under aliases also
for alias in aliases:
self._name_parser_map[alias] = parser

文字列のコマンド名と、今作ったオブジェクトを紐付けます。

これにより、ユーザーがターミナルで add と打った際、

どのオブジェクトを使って解析すればよいかプログラムが判断できるようになります。

別名がある場合もここで同じオブジェクトを紐付けます。

6. 完了と返却

        if deprecated:
self._deprecated.add(name)
self._deprecated.update(aliases)

return parser

セットアップが終わった子パーサーを返します。

子パーサーが受け取るデータを決める

add_parser.add_argument("title",help="予定のタイトル")
add_parser.add_argument("due",help="期限(YYYY-MM-DD)")

子パーサーに対して、「具体的にどんなデータを受け取るか」という詳細ルールを決めています。

🌸「位置引数(Positional Argument)」の定義

"title" のように、ハイフン(-)を付けずに名前を指定すると、それは「位置引数」になります。

例えば、 python3 main.py add "研修""研修" がこの title に吸い込まれます。

🌸変数名の確定

ここで指定した "title" という名前が、そのままプログラムの中でデータを取り出す時の名前になります。

※後ほど args = parser.parse_args() を実行した際、args.title と書くだけで中身(例: “研修”)にアクセスできるようになります。

🌸ヘルプメッセージの紐付け

help="予定のタイトル" は、ユーザーが使い方が分からなくなった時のための注釈です。

python3 main.py add -hを叩いた時に、「title:予定のタイトル」と親切に表示されるようになります。

argparse.py(標準ライブラリ)

🌸「位置引数」か「オプション引数」かの自動判定

        chars = self.prefix_chars
if not args or len(args) == 1 and args[0][0] not in chars:
if args and 'dest' in kwargs:
raise TypeError('dest supplied twice for positional argument,'
' did you mean metavar?')
kwargs = self._get_positional_kwargs(*args, **kwargs)
    else:
kwargs = self._get_optional_kwargs(*args, **kwargs)

self.prefix_chars(デフォルトは -)を取得し、args[0][0](引数の1文字目)がそれに含まれるかを if 文で判定しています。含まれなければ「位置引数(positional)」、含まれれば「オプション引数(optional)」として、それぞれ別の内部メソッドへ処理を飛ばしています。

🌸引数の保存先(dest)とデフォルト値の確定

        if 'default' not in kwargs:
dest = kwargs['dest']
if dest in self._defaults:
kwargs['default'] = self._defaults[dest]
elif self.argument_default is not None:
kwargs['default'] = self.argument_default

引数を受け取る変数名 dest をキーにして、辞書 self._defaults から値を探しています。これにより、個別にデフォルト値を指定しなくても、パーサー全体で決めたルールを適用する一貫性を保っています。

🌸Actionオブジェクトの生成と検証

        action_name = kwargs.get('action')
action_class = self._pop_action_class(kwargs)
if not callable(action_class):
raise ValueError(f'unknown action {action_class!r}')
action = action_class(**kwargs)

設定情報(kwargs)を元に action_class を決定し、そのインスタンス(action)を生成しています。

self._check_help(action)
return self._add_action(action)

最後は _check_help でヘルプ画面用の検証を行い、_add_action で正式なリストに登録して終了します。

サブコマンド(list)のパーサーを作る

subparsers.add_parser("list",help="予定一覧を表示")

先ほどはaddのパーサーを作りましたが、今度はlistのパーサーを作ります。

実際に入力された引数を仕分けする

args = parser.parse_args()

ここまでで作ってきたサブコマンドをargs.titleやargs.dueという形で表せるようにします。

argparse.py(標準ライブラリ)

🌸「未知の引数」がないか最終チェック

parse_known_args を呼び出し、「自分が知っている引数(args)」と「解析できなかった余計な文字(argv)」に切り分けます。

🌸argv(解析できなかった余計な文字)への処置

もし argv(余計な文字)の中身が空でなければ、即座にエラーメッセージを作成します。

argparse のデフォルト設定が「定義されていない文字が入っていたら即座に実行を止める」という厳格な仕様になっているのは、この数行があるからです。

例: python3 todo.py add "研修" --unknown と打った際、--unknown がこの argv に入り、エラーとして弾かれます。

実行結果


まとめ

普段何気なく使っている標準ライブラリの内部実装を読み解くことで、ブラックボックスだった処理がロジックとして繋がる面白さを体感できました。

ライブラリを単なる便利な道具として使うだけでなく、その裏側で動いている設計に触れることができ、視野が広がりました。

ありがとうございました!

今回登場したコード全体

import sqlite3
import argparse
from datetime import datetime

def init_db():
with sqlite3.connect("schedule.db") as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS task(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
due_date TEXT NOT NULL,
status TEXT DEFAULT '未完了'
)
""")

def add_task(title,due_date):
try:
datetime.strptime(due_date,"%Y-%m-%d")
with sqlite3.connect("schedule.db") as conn:
conn.execute("INSERT INTO task (title,due_date) VALUES(?,?)",(title,due_date))
print(f"予定を登録しました{title}{due_date}")
except ValueError:
print("エラーが発生しました")

def list_task():
print(f"{'ID':<4} | {'予定':<20} | {'期限':<12} | {'状態'}")
with sqlite3.connect("schedule.db") as conn:
cursor = conn.execute("SELECT * FROM task ORDER BY due_date ASC")
for row in cursor:
print(f"{row[0]:<4} | {row[1]:<20} | {row[2]:<12} | {row[3]}")

def update_task(task_id):
with sqlite3.connect("schedule.db") as conn:
cursor = conn.execute(
"UPDATE task SET status = '完了' WHERE id = ? ",(task_id,))
print(f"ID {task_id} の予定を『完了』に更新しました!")

def main():
# db接続
init_db()
# argparse.pyのArgumentParserクラスをインスタンス化する
parser = argparse.ArgumentParser(description="ツール")
# サブパーサー作成
subparsers = parser.add_subparsers(dest="command")
# 追加用のサブパーサーを作る
add_parser = subparsers.add_parser("add",help="予定を追加")
# 追加用のサブパーサーに具体的なデータを入れる
add_parser.add_argument("title",help="予定のタイトル")
# 追加用のサブパーサーに具体的なデータを入れる
add_parser.add_argument("due",help="期限(YYYY-MM-DD)")
# 表示用のサブパーサーを作る
subparsers.add_parser("list",help="予定一覧を表示")
# 追加用のサブパーサーに更新の処理を加えるパーサーを作る
update_parser = subparsers.add_parser("update", help="予定を完了にする")
# 更新用のサブパーサーに具体的な更新内容を入れる
update_parser.add_argument("id", type=int, help="完了にする予定のID")
# 引数を渡す
args = parser.parse_args()

if args.command == "add":
add_task(args.title,args.due)
elif args.command == "list":
list_task()
elif args.command == "update":
update_task(args.id)
else:
parser.print_help()

if __name__ == "__main__":
main()
タイトルとURLをコピーしました