INVESTMENT #08

配当金管理ツールを作る(tkinter × SQLite)

Python経験者向けの投資分析シリーズ第8回。受け取った配当をGUIで入力してSQLiteに貯め、銘柄別・月別に集計するツールを作ります。3.12で非推奨になった日付の渡し方金額をfloatで持つ危険も、実行結果つきで扱います。

🎯 対象: Python経験者・投資分析 📦 Python 3.12.10 / SQLite 3.49.1 / Tcl-Tk 8.6.15 🧪 2026-09-01 に実行して確認 ⏱️ 読了: 約18分

1. この記事のゴールと連載の位置づけ

本記事は「Python経験者が、自分の投資ポートフォリオを管理・可視化するアプリを作る」連載の第8回です。第5〜7回で可視化の部品がそろったので、ここからはデータを貯める側へ。受け取った配当を手で入力して記録し、銘柄別・月別に集計するtkinterのGUIアプリを作ります(保存先はSQLite・追加インストール不要)。

配当金管理ツールの画面。上部に入金日・銘柄・株数・1株配当・入金額の入力フォーム、下部に明細のTreeviewが8行並び、右下に2026年の入金額合計37,132円(5件)と表示されている。金額はすべて架空の入力例
▲ 完成形。入力・一覧・集計が1画面(金額はすべて架空の入力例)

「配当の記録ならExcelで足りている」——銘柄が5つで今年ぶんしか見ないなら、そのとおりです。崩れるのは年をまたいだときと銘柄が増えたとき。シートを年で分けると「3年分の銘柄別合計」が出せず、銘柄名を手打ちすれば「トヨタ自動車」と「トヨタ自動車㈱」が別物になる——データの持ち方の問題です。

既製の配当管理アプリではなく自作する理由

配当管理の既製アプリはいくつもあります。それでも自作を選ぶ理由は3つで、どれもこの記事のあと自分に残るものです。

  • 資産の記録を自分のPCの中だけで完結できる——保存先は dividends.db 1ファイルだけで、外部との通信はありません(クラウドに預ける形のサービスを使うかどうかは好みの問題です。FAQでも触れます)
  • 列を自分で足せる——「証券会社」「メモ」など、見たい切り口を後からスキーマに追加できる
  • 次の分析コードにそのままつながる——貯めた表は第7回の円グラフや以降の集計にそのまま渡せる

裏返すと、作る手間(この記事ぶん)に見合うのは、年をまたいで貯めたい人・分析まで自分でやりたい人です。

🎯
この回のゴール

配当の入金記録をSQLiteに貯め、銘柄別・月別・年間で集計して画面に出すところまで。データは自分のPCの1ファイル(dividends.db)に置き、ネットワークには出しません。本文には要点のコードだけを載せ、全部をつないだ完成版は dividend_app.py にあります。3〜4章は、先に踏むと後が楽になる地雷の実演です。

🧪
この記事の検証環境と、画像の撮り方

本文のコード・出力・画面写真は Windows 11 / Python 3.12.10 / SQLite 3.49.1 / Tcl-Tk 8.6.152026年9月1日に実行して確認したものです。tkinterの画面は保存機能を持たないので、アプリを起動してWindows APIの PrintWindow で描画させ、PNGにしました(Pillow 12.2.0・合成や加筆はなし)。

登録している配当金額・株数はすべて架空の入力例です。銘柄コードと会社名は実在のものですが実際の配当実績とは無関係で、推奨銘柄でも運用実績でもありません。

2. どんなテーブルにするかを先に決める

GUIより先にテーブルを決めます。列は後から足せますが、型を間違えると全レコード作り直しだからです。

CREATE TABLE IF NOT EXISTS stocks (
    ticker    TEXT PRIMARY KEY,          -- '7203.T' のような識別子
    name      TEXT NOT NULL,
    name_kana TEXT NOT NULL              -- 読み(並べ替え用・7章で効く)
);
CREATE TABLE IF NOT EXISTS dividends (
    id            INTEGER PRIMARY KEY,
    paid_on       TEXT    NOT NULL,      -- 入金日 'YYYY-MM-DD' 固定
    ticker        TEXT    NOT NULL REFERENCES stocks(ticker),
    shares        INTEGER NOT NULL,
    per_share_sen INTEGER NOT NULL,      -- 1株あたり配当(銭=円×100)
    net_sen       INTEGER NOT NULL,      -- 実際の入金額(銭)
    created_at    TEXT    NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE INDEX IF NOT EXISTS idx_div_paid_on ON dividends(paid_on);

銘柄を別テーブルに切り出したのは表記ゆれを構造的に潰すため。登録済みの銘柄からしか選べなければ、打ち間違えようがありません。

金額は「銭の整数」で持つ

いちばん効く判断がここです。SQLiteに金額型はありません。ストレージクラスはNULL・INTEGER・REAL・TEXT・BLOBの5つだけ。DECIMAL(10,2) と書いても10進小数にはならず、REALは公式のとおり8バイトのIEEE 754浮動小数点です。

そこで最小単位の整数で持ちます。円ではなく銭(円×100)にしたのは1株あたりの配当が小数になるから。列名を per_share_sen単位が名前に出る形にすれば「円か銭か」で悩む事故も消えます。

日付は 'YYYY-MM-DD' の文字列で持つ

日付型もありません。TEXT(ISO-8601)・REAL(ユリウス日)・INTEGER(Unix時間)から選びます。ゼロ埋めしたTEXTなら ORDER BY がそのまま日付順になり、目で読めるのが利点。ただし書式を固定しないと集計が壊れます

⚠️
税引後の額は「計算」ではなく「手入力」にした

net_sen実際に振り込まれた金額を手で入れる列で、税引前からの自動計算はしません。上場株式等の配当等には源泉徴収の仕組みがありますが、税率や課税方式は口座の種類・受取方法・その年の制度で変わり、端数処理も一律ではないからです。アプリは税額を一切計算しません。制度の内容は国税庁のNo.1330 配当金を受け取ったとき(配当所得)でご確認いただくか、税理士にご相談ください。

3. つまずき実演:日付をそのまま渡すと3.12で警告が出る

入金日は datetime.date で扱いたくなります。素直に書くとこうです。

import sqlite3, datetime

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE dividends(paid_on, ticker, amount)")
conn.execute("INSERT INTO dividends VALUES (?, ?, ?)",
             (datetime.date(2026, 6, 26), "7203.T", 1000))
conn.execute("INSERT INTO dividends VALUES (?, ?, ?)",
             (datetime.datetime(2026, 6, 26, 10, 30), "6758.T", 2000))
print(conn.execute("SELECT paid_on FROM dividends").fetchall())
exp.py:5: DeprecationWarning: The default date adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes
exp.py:7: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes
[('2026-06-26',), ('2026-06-26 10:30:00',)]

※ 実際の出力では各警告の下に該当ソース行のエコーが1行ずつ入ります(ここでは省いています)。

動きますが、警告が利用者の画面に出ます。DeprecationWarning が普段は隠れているのに見えるのは、既定フィルタが __main__ から直接発生した警告だけを表示するからです(warningsのドキュメント)。python app.py と直接叩くアプリはまさにその条件です。

非推奨になったのは既定のadapter と converterです。adapterはPythonの値をSQLite用に変換する仕組みで、converterはその逆。対象は datedatetime のadapterと、宣言型 datetimestamp のconverterの4つです。公式は「recipesを自分の用途に合わせて使え」としています。

ただし公式レシピに乗り換えるとDBに入る文字列が変わります。旧・既定のadapterは isoformat(" ")(スペース区切り)、レシピの adapt_datetime_isoisoformat()T区切り。実行すると '2026-06-26 10:30:00''2026-06-26T10:30:00' になりました。事故るのは前後のデータが混ざったときです。

書式が混ざると、静かに集計から落ちる

「2026年7月10日」のつもりの5通りを同じ列に入れ、月別集計をかけます。

for r in tmp.execute("SELECT paid_on, strftime('%Y-%m', paid_on), date(paid_on) FROM d"):
    print(r)
('2026-07-10',          '2026-07', '2026-07-10')
('2026/07/10',          None,      None)
('2026-7-10',           None,      None)
('2026-07-10 00:00:00', '2026-07', '2026-07-10')
('2026-07-10T00:00:00', '2026-07', '2026-07-10')

-- GROUP BY strftime('%Y-%m', paid_on) → None が2件、'2026-07' が3件
-- WHERE paid_on BETWEEN '2026-07-01' AND '2026-07-31' → 5件のうち3件だけ

5件が2つのグループに割れました2026/07/102026-7-10 は日時関数が解釈できず None に落ちます。公式が「ISO-8601には多様な書式があるが、SQLiteは列挙したものだけをサポートする」と明記しているとおりゼロ埋めは必須です。

厄介なのは BETWEEN で、こちらは3件しか拾いません。文字列比較なので '2026/07/10''/'(U+002F)が '-'(U+002D)より大きく、'2026-7-10' は6文字目の '7''0' より大きく、どちらも上限の '2026-07-31' を超えて範囲の外へ出ます。集計方法で件数が変わるのに、どちらもエラーを出しません。対策は date.fromisoformat(text).isoformat() を入口に置くだけ。上の2つは ValueError で弾かれます。

🌏
date('now') はUTC。DEFAULT値にすると日付がずれる

SQLiteの日時関数は既定でUTCです。実行すると datetime('now')2026-09-01 03:10:48datetime('now','localtime')2026-09-01 12:10:48 と9時間ずれました。つまり日本時間の0時から9時に入力すると date('now') は前日を返します。スキーマを DEFAULT (datetime('now','localtime')) にし、入金日はPython側で date.today().isoformat() を渡します

4. つまずき実演:金額をfloatで持つと検算が通らない

もう1つの地雷が金額の型です。0.01〜4999.99円の架空の配当100件をfloatとDecimalで合算します。

random.seed(20260901)
vals = [Decimal(random.randrange(1, 500000)) / 100 for _ in range(100)]
f = 0.0
for v in vals:
    f += float(v)
print(repr(f), sum(vals), f == float(sum(vals)))
253836.41999999995 253836.42 False

ズレは約6×10-11。表示を丸めれば画面上は同じですが、効くのは「一致しない」ほうで、明細の合計と集計値を == で突き合わせる検算が落ちます。0.1 を10回足すと 0.9999999999999999 になるのと同じです。

ではDecimalをそのままDBに入れられるか

conn.execute("INSERT INTO t VALUES (?)", (Decimal("1234.56"),))
# ProgrammingError: Error binding parameter 1: type 'decimal.Decimal' is not supported

入れられません。sqlite3が素で扱える型は Noneintfloatstrbytes だけ。adapterを登録するか、アプリ側で整数に直して渡すことになります(ProgrammingError のような例外そのものの読み方はPythonのよくあるエラーと対処法にまとめています)。

「TEXTで保存すれば精度は守れる」の落とし穴

文字列で入れれば精度は保てる——半分正しく、集計した瞬間に壊れます。同じ値をTEXT列と銭のINTEGER列で SUM() した結果です。

SELECT SUM(amount),     typeof(SUM(amount))     FROM text_div;  -- (79685.0, 'real')
SELECT SUM(amount_sen), typeof(SUM(amount_sen)) FROM sen_div;   -- (7968500, 'integer')

SQLiteの集約関数の仕様に「非NULLの入力がすべて整数なら sum() の結果は整数値。整数でもNULLでもない入力が1つでもあれば、結果は近似である浮動小数点値になる」とあるとおりです。TEXTで持つと集計をPython側に持ち出すことになり、GROUP BY が使えません。

持ち方精度SQLで SUM()判断
INTEGER(銭=円×100)完全できる(整数のまま)これを採用。表示時に Decimal(v)/100 で戻す。円単位だと1株12.5円が持てない
TEXT(10進文字列)Python側では完全実質できないORDER BY も文字列順になる
REAL誤差ありできる(誤差込み)採らない

入口と出口に変換関数を置けば、他は整数だけで済みます。

from decimal import Decimal, InvalidOperation

def yen_to_sen(text):
    """円の文字列 -> 銭(int)。1銭未満は ValueError。"""
    try:
        sen = Decimal(str(text).strip().replace(",", "")).scaleb(2)
    except InvalidOperation:
        raise ValueError(f"金額として読めません: {text!r}")
    if sen != sen.to_integral_value():
        raise ValueError(f"1銭未満は扱えません: {text!r}")
    return int(sen)

def sen_to_yen(sen):
    """銭(int) -> 表示用の円文字列。956200 -> '9,562'。"""
    return f"{Decimal(sen) / 100:,}"

実行すると yen_to_sen('12.5')1250'12.345''abc'ValueErrorsen_to_yen(956200)'9,562'Decimal("12.5")文字列から作るのが要点で、Decimal(12.5) だと誤差を引き継ぎます。なお0件のとき SUM()NULLを返すので、集計は COALESCE(SUM(net_sen), 0) で包みます。

5. SQLite層を書く(接続・コミット・パラメータ化)

DB層は読み書きの入口を2つ用意するだけです。

import sqlite3
from contextlib import closing
from pathlib import Path

DB_PATH = Path(__file__).with_name("dividends.db")

def connect():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row          # 列名でアクセスできるようにする
    conn.execute("PRAGMA foreign_keys = ON")
    return conn

def query(sql, params=()):
    """読み取り専用。closing() で必ず接続を閉じる。"""
    with closing(connect()) as conn:
        return conn.execute(sql, params).fetchall()

def execute(sql, params=()):
    """書き込み。closing()=閉じる/with conn=コミット(例外ならロールバック)。"""
    with closing(connect()) as conn, conn:
        return conn.execute(sql, params).lastrowid

登録用の add_dividend() は、この execute()INSERT INTO dividends ... VALUES (?, ?, ?, ?, ?) と値を渡す薄い関数。起動時に呼ぶ init_db()conn.executescript(SCHEMA) でテーブルを作り、stocks が空のときだけ銘柄マスタの初期値を入れますIF NOT EXISTS と件数チェックで、毎回呼んでも二重登録になりません)。

with sqlite3.connect(...) は接続を閉じない

with closing(connect()) as conn, conn: という2段重ねには理由があります。with open(...) と同じつもりで with sqlite3.connect(...) と書いても接続は閉じません(抜けたあとに conn.execute("SELECT 1") がそのまま通ります)。公式ドキュメントも「暗黙に新しいトランザクションを開くこともなければ、接続を閉じることもしない。閉じるなら contextlib.closing() の利用を検討せよ」(英文からの日本語訳は当サイトによるもの・以下同じ)と明記しています。

書き方抜けるときにすること
with conn:正常終了なら commit()、未捕捉の例外なら rollback()接続は開いたまま
with closing(conn) as c, c:closingclose()c がコミット(または巻き戻し)担当

ロールバックも効きます。with closing(...) as c, c: の中でINSERTしてから例外を投げ、別の接続で数え直すと残った行数は02文以上を1操作として扱うときに、片方だけ書き込まれた状態が残りません。

値は必ず ?(または :name)で渡し、文字列連結で組み立てないのも大事です(公式も「SQLインジェクション攻撃に対して脆弱なので、クエリの組み立てにPythonの文字列操作を使うのは避けよ」と警告)。:name 形式は必ず辞書で渡してください。シーケンスを渡す書き方は3.12で DeprecationWarning、3.14で ProgrammingError になります。

集計クエリは定数にしておきます。月別のキーは strftime('%Y-%m', paid_on)、税引前の額は保存せず SUM(shares * per_share_sen) で計算します(整数×整数なので整数のまま)。

6. tkinterで入力フォームを作る

入力欄は入金日・株数・1株配当・入金額の4つ。これに銘柄のComboboxが付きます。

    # FIELDS = [(属性名, ラベル, 行, 列, 入力チェック), ...] を4つ分ならべておく
    def _build_form(self):
        box = ttk.LabelFrame(self, text="配当を登録する", padding=10)
        checks = {"digits": (self.register(lambda p: p == "" or p.isdecimal()), "%P"),
                  "amount": (self.register(self._is_amount), "%P")}
        for attr, label, row, col, check in self.FIELDS:
            var = tk.StringVar()
            setattr(self, attr, var)
            entry = ttk.Entry(box, textvariable=var, width=14)
            if check:                                  # ← 1文字ごとの検証を有効にする
                entry.configure(validate="key", validatecommand=checks[check])
            entry.grid(row=row, column=col + 1, sticky="w", padx=4, pady=4)
        self.v_date.set(date.today().isoformat())

        self.v_stock = tk.StringVar()                  # 銘柄は選択式(打ち込ませない)
        self.combo = ttk.Combobox(box, textvariable=self.v_stock, state="readonly",
                                  width=32, values=list(self.label_to_ticker))
        self.combo.grid(row=0, column=3, sticky="w", padx=4, pady=4)

    @staticmethod
    def _is_amount(p):        # 小数点は1つまで
        return p in ("", ".") or p.count(".") <= 1 and p.replace(".", "").isdecimal()

ラベルとボタンの配置は省いています。

Comboboxを state="readonly" にする理由

見た目ではなくデータ品質の話です。readonly ならリストからの選択しかできず、1章の表記ゆれ問題が入力段階で消えます。選択肢は銘柄マスタから作り、表示名からティッカーを引ける辞書にします。

self.label_to_ticker = {f"{r['name']}({r['ticker']})": r["ticker"]
                        for r in query("SELECT ticker, name FROM stocks"
                                       " ORDER BY name_kana")}

readonly でもプログラムからの set() は効きます(禁じられるのは利用者の直接入力だけ)。values に無い値をセットすると current()-1 を返すので、銘柄を追加したら combo["values"] も入れ替えます。

入力検証は「その場」と「送信時」の二段構え

validate="key"1文字打つたびに呼ばれる検証で、関数が False を返すとその入力が反映されません。%P は「編集後の値」、self.register() はPythonの関数をTcl側から呼べる名前に変える仕組みです。ただしできるのは文字種のふるい分けまでです(株数の欄に「12.5」は入りませんが、値の意味までは見ていません)。入金日にいたっては FIELDS の検証が Noneそもそも掛けていないので「2026-99-99」も打ててしまいます。だから登録ボタンを押した時点でもう一度検証します。

def on_submit(self):
    try:
        paid_on = date.fromisoformat(self.v_date.get().strip()).isoformat()
    except ValueError:
        messagebox.showerror("入力エラー", "入金日は 2026-06-26 の形式で入力してください。")
        return
    ticker = self.label_to_ticker.get(self.v_stock.get())
    if ticker is None:
        messagebox.showerror("入力エラー", "銘柄を選んでください。")
        return
    try:
        shares = int(self.v_shares.get())
        per_share_sen = yen_to_sen(self.v_per_share.get())
        net_sen = yen_to_sen(self.v_net.get())
    except ValueError as e:
        messagebox.showerror("入力エラー", str(e))
        return
    add_dividend(paid_on, ticker, shares, per_share_sen, net_sen)
    self.reload()

2026/6/26 と入れて「登録する」を押すと、DBに触れる前に止まります。

Windowsのエラーダイアログ。タイトルは「入力エラー」、本文に「入金日は 2026-06-26 の形式で入力してください。」と表示されている
▲ DBに触れる前に止まる

yen_to_sen()ValueError をそのままダイアログに出すので、検証のルールは変換関数の1か所にまとまります

なおtkinterにも ttk にも日付ピッカーはありませんtkcalendarDateEntry)はありますが、PyPIの最新リリースは1.6.1・公開日2019年12月28日と更新が止まって6年以上です(2026年9月1日確認)。依存を増やさない方針なので、テキスト入力+fromisoformat() の検証で通しています。

7. Treeviewで集計を見る(並べ替えと日本語の罠)

ttk.Treeview を3つ作り、明細・銘柄別・月別をタブで切り替えます。

def make_tree(parent, columns):
    """(列ID, 見出し, 幅, 寄せ, 並べ替えキー) のリストから Treeview を組み立てる。"""
    frame = ttk.Frame(parent)                       # grid の配置・weight 設定は省略
    tree = ttk.Treeview(frame, columns=[c[0] for c in columns],
                        show="headings", selectmode="browse", height=10)
    for cid, text, width, anchor, key in columns:
        tree.heading(cid, text=text,
                     command=lambda c=cid, k=key: sort_column(tree, c, False, k))
        tree.column(cid, width=width, anchor=anchor, stretch=(cid == "name"))
    bar = ttk.Scrollbar(frame, orient="vertical", command=tree.yview)
    tree.configure(yscrollcommand=bar.set)
    tree.tag_configure("thisyear", background="#eef6ff")
    return tree

外すと詰まる指定が4つ。show="headings" は左端の謎の空列を消すもの(列#0は常にツリー列で、指定しないと出たまま)。command=tree.yviewyscrollcommand=bar.set はスクロールバーとの相互接続で片方だけだと動きません。lambda c=cid, k=key: は遅延束縛対策、tag_configure は行の色分けです。

見出しクリックで並べ替える(自作)

Treeviewには並べ替え機能がありません。Python公式にもTkDocsにも完成コードは無く、あるのは部品だけ。heading(command=...)get_children()set()move() を組み合わせます。

def sort_column(tree, col, descending, key):
    """見出しクリックで並べ替える(自作。move() で行の位置を入れ替えるだけ)。"""
    rows = sorted(((key(tree.set(iid, col)), iid) for iid in tree.get_children("")),
                  reverse=descending)
    for index, (_, iid) in enumerate(rows):
        tree.move(iid, "", index)
    tree.heading(col, command=lambda: sort_column(tree, col, not descending, key))

最後の1行が昇順・降順のトグル(次のクリック時の向きを反転して再登録)です。

key を渡す理由は実測するとはっきりします。values"10,757"12345(int)を入れて取り出すとどちらも str で返ります。Tcl側の値がすべて文字列だからで、そのまま並べ替えると辞書順です。

素の sort(): ['10,757', '12345', '3,984', '6,574']

金額列だけ float(s.replace(",", "")) に通すキー関数を渡せば直ります。並べ替えてもDBには何も起きません。

日本語の ORDER BY は読み順にならない

五十音順に並べたくなりますが、ORDER BY name だとこうなります。

みずほフィナンシャルグループ    (み U+307F)
トヨタ自動車                  (ト U+30C8)
三菱UFJフィナンシャル・グループ (三 U+4E09)
日本電信電話                  (日 U+65E5)
第一三共                      (第 U+7B2C)

SQLiteの既定の照合順序は BINARYmemcmp() によるバイト比較。UTF-8ではコードポイント順になるため、ひらがな→カタカナ→漢字の順に並び読み順にはなりませんNOCASE公式が「大文字小文字が畳まれるのはASCII文字だけ」と明記しているとおり日本語には効きません。

だから2章のスキーマに name_kana がありました。ORDER BY name_kana なら「第一三共 → トヨタ自動車 → 日本電信電話 → みずほ → 三菱UFJ」と読み順です。並べ替えのために列を1つ足すのは設計段階でしか安く入れられません。

集計結果

銘柄別タブの画面。三菱UFJフィナンシャル・グループ2件21,036円、トヨタ自動車2件20,319円、日本電信電話2件13,148円などが入金額の多い順に並んでいる。金額はすべて架空の入力例
▲ 銘柄別(金額はすべて架空の入力例)
月別タブの画面。2025-12が3件で税引前33,050円・入金額26,335円、2026-03が1件、2026-06が4件で税引前41,600円・入金額33,148円と並んでいる。金額はすべて架空の入力例
▲ 月別(金額はすべて架空の入力例)

年をまたいだ集計が1クエリで出るのが、シートを分けるやり方との違いです。

8. 組み立てて動かす

2〜7章で出したのがアプリの中核で、残りはタブを組み立てる _build_tabs() とクエリ結果を流し込む reload() の配線だけ。完成版が dividend_app.pyで、保存して python dividend_app.py で起動します。

def main():
    init_db()
    root = tk.Tk()
    root.title("配当金管理ツール")
    root.geometry("960x470")
    DividendApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()

初回起動時は init_db()銘柄マスタの初期値を5件だけ入れるので、Comboboxが空で何もできない状態にはなりません(完成版の SEED_STOCKSdividend_app.py)に並べた銘柄コードに推奨の意味はありません。自分の保有銘柄に書き換えてください)。あとから足すときは同じフォルダに置いたスクリプトから呼びます。

from dividend_app import init_db, execute

init_db()
execute("INSERT OR IGNORE INTO stocks VALUES (?, ?, ?)",
        ("6758.T", "ソニーグループ", "ソニーグループ"))

入れるのは銘柄マスタだけで、配当の明細はアプリの画面から手で入れます。

中身を直接のぞくなら、Python 3.12から標準でSQLiteのシェルが付いていますpython -m sqlite3 dividends.dbsqlite3 shell, running on SQLite version 3.49.1 と出てSQLを打てるので、表示がおかしいとき原因がSQL側かGUI側かを切り分けられます。

9. つまずきやすいポイントと次につなげる

自分のデータを入れると出てくる問題です。

つまずき何が起きるか対処
日付の書式が混ざるstrftimeNone グループ、BETWEEN は件数が減る。どちらもエラーなし入口で fromisoformat() を通して正規化(3章)
金額をfloatやTEXTで持つ合計の == 検算が落ちる/SUM() がREALになる銭のINTEGERで保存(4章)
with sqlite3.connect(...) だけで安心する接続が閉じずに残るclosing() と2段重ねにする(5章)

上の2つはエラーが出ないぶん、気づくきっかけがありません。総件数と集計結果の件数合計が一致するかを時々確かめてください。

配当の自動取得は次回に

「1株配当くらい自動で取れないのか」——取れる値と記録したい値が違います。第1回で使ったyfinanceには Ticker.dividends があります。

s = yf.Ticker("6758.T").dividends     # yfinance 1.4.1・2026-09-01 実行
print(s.tail(2))
Date
2025-09-29 09:00:00+09:00    12.5
2026-03-30 09:00:00+09:00    12.5
Name: Dividends, dtype: float64

返るのはタイムゾーン付きの DatetimeIndex を持つ pandas.Series。ただし日付は権利落ち日で入金日ではなく、金額は1株あたりの税引前です。家計簿に入れたいのは「いつ・いくら振り込まれたか」なので、そのままでは使えません。自動取得は手入力値との突き合わせ用という位置づけで、次回(第9回)の「配当金カレンダーを自動生成する」で扱う予定です。正はご自身の取引報告書です。

なお、この出力だけは架空値ではなく2026年9月1日に取得した実データの実行例です。yfinance はPyPIの説明で「調査・教育目的を意図している」「Yahoo! finance の API は個人利用のみを意図している」(当サイト訳)と明示しています。再配布や業務利用の前に、必ず自分で最新の利用条件を確認してください(番外編にもまとめています)。

この形で持っておくと、あとの回が楽になります。評価額の計算は第4回のPythonで保有銘柄の損益を計算する(pandas)、業種の分類は番外編のyfinanceでセクター情報を取得するがそのまま乗ります。CSVから一括投入するなら、第3回の証券会社のCSVをpandasで読み込むで読んだDataFrameを、列順のタプルに並べ替えて円→銭に直せば executemany() にそのまま渡せるはずです。tkinterが初めてならToDoリストアプリ、DBの中身を見る道具ならSQLiteデータベースブラウザもどうぞ。

📖
次に読むなら:自分のツールを「壊れにくく」する

この記事で効いたのは Treeview の書き方ではなく型と境界の決め方——金額をどの型で持つか、変換をどこに置くか、コンテキストマネージャに何をさせるか——でした。この判断を鍛えるならEffective Python 第3版が近道です(当サイトのおすすめ本ランキング総合4位・全572ページ)。リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みます。

📌
この連載のスタンス

本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではなく、記事中の配当金額・株数はすべて架空の入力例です。当サイトは投資助言業(金融商品取引業)の登録を行っておらず、税務相談にも応じられません。税制の適用や確定申告の要否は国税庁の情報か税理士にご確認ください。投資判断はご自身の責任で(詳細は免責事項)。

10. よくある質問(FAQ)

Q. DeprecationWarning: The default date adapter is deprecated と出ます。どう直しますか?

3.12で date / datetime の既定adapterが非推奨になったためです。渡す前に date.isoformat() で文字列にするのがいちばん簡単で、本記事もこの方法。公式の「Adapter and converter recipes」を登録する道もありますが、保存される文字列がT区切りに変わるので既存DBの書式混在に注意してください。

Q. 金額はDecimalで持つべきですか? それとも整数ですか?

DBには整数(最小単位)で保存し、計算・表示のときだけDecimalに変換するのが扱いやすいです。Decimalexecute() に直接渡すと ProgrammingError: Error binding parameter 1: type 'decimal.Decimal' is not supported、文字列で保存すると SUM() が浮動小数点値を返します。円未満が出るなら銭(円×100)を単位にし、列名も本記事の per_share_sen / net_sen のように単位入りにしておくと事故が減ります。

Q. with sqlite3.connect(...) as conn: と書けば接続は閉じますか?

閉じません。公式ドキュメントに「暗黙に新しいトランザクションを開くこともなければ、接続を閉じることもしない」(当サイト訳)と明記されています。with conn: がやるのは正常終了時の commit() と例外時の rollback() だけ。閉じたいなら with closing(sqlite3.connect(path)) as conn, conn: と書きます。

Q. Treeviewの列見出しをクリックしても並べ替わりません。金額の列も順番がおかしいです。

Treeviewに並べ替え機能は組み込まれていませんheading(col, command=...) にコールバックを登録し、get_children() で取り出して move(iid, "", index) で入れ替える処理を自分で書きます(lambda c=col: と束縛しないと全列が最後の列を並べ替えます)。金額が辞書順になるのは tree.set()常に文字列を返すためで、key に数値変換を渡せば直ります。

Q. 銘柄名で並べ替えても五十音順になりません。

SQLiteの既定の照合順序が BINARY(バイト比較)だからです。UTF-8ではコードポイント順になり、ひらがな→カタカナ→漢字の順に並びます(NOCASE が畳むのはASCII文字だけ)。読みの列で ORDER BY/ティッカーで並べる/Python側で並べ替えるの3択で、本記事は1つめです。

Q. 貯めた配当データは、どこかにアップロードされますか?

されません。完全にローカルで動作します。データはスクリプトと同じフォルダの dividends.db 1ファイルに保存されるだけで、外部との通信はありません(9章のyfinanceの例のみ通信しますが、送るのは銘柄コードだけです)。データを自分の手元だけに置いておけるのが、自作を選ぶ実利のひとつです(既製サービスの保存先はサービスごとに違うので、使うときはそれぞれの規約をご確認ください)。

🧰
読み終えたら:次に手を動かす3本

まずは完成版 dividend_app.py を保存し、python dividend_app.py で起動して自分の配当を1件入れてみてください(銘柄マスタは初回起動時に入ります)。手を動かす続きはMatplotlibで円グラフを作る(第7回)と在庫管理アプリ(SQLite)。連載の全体像は投資×Python シリーズ一覧です。続きが出たときに気づけるよう、この一覧ページをブックマークしておくのが確実です。更新は新着記事のRSSでも配信しています。

本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。掲載しているコード・出力・画面写真は Windows 11 / Python 3.12.10 / SQLite 3.49.1 / Tcl-Tk 8.6.15 / yfinance 1.4.1 / pandas 3.0.3 で 2026年9月1日に実行して確認したものです。銘柄コードと会社名は実在のものですが、登録している配当金額・株数はすべて架空の入力例で、実際の配当実績とは関係がありません。本記事はプログラミングの情報提供を目的としたもので、銘柄の売買・投資手法を推奨するものでも税務上の助言でもありません。投資判断はご自身の責任で行ってください。AIの利用方針は免責事項をご覧ください。