コンテンツにスキップ

Streamlit 基本 用語集+学習メモ — 写経で詰まったところ

ex01〜ex08 を写経しながら「Streamlit の挙動が腑に落ちない/言葉が混乱する」ポイントを1枚にまとめた学習メモ+用語集。 サンプルの並び・起動方法は README.md を参照。 連載側の本体は第14回(Streamlit 入門)/ 第18回(チャート)/ 第22回(画像アップロード + HITL)/ 第31回(Middleware 可視化)に対応。

このフォルダには 2 系統のファイルがある。混同しないこと。

系統 ファイル名 性格
注釈つきテンプレート ex01_hello.py / ex02_widgets.py / ex03_session_state.py / ex04_chat_echo.py / ex05_streaming_fake.py / ex06_llm_chat.py / ex07_llm_chat_streaming.py / ex08_file_upload.py docstring と # ①②③ 番号コメントで原理を厚く説明した「お手本」
写経(手で書き写した版) ex01.pyex07.py お手本を見ながら自分でタイプし直したもの。コメントは薄め。ex07.py はお手本とほぼ同一

ex08 は写経版がなく ex08_file_upload.py のみ。どちらの系統も streamlit run <ファイル> で個別に起動できる。


全体像 — 何を、どの順で学ぶか

ex01 から ex07 まで、前のサンプルの概念を必ず次が使う積み上げ構成になっている。ex08 は ex02 の派生(入力ウィジェットの一種としてのファイルアップロード)。

テーマ 中心 API 前提にしている回
ex01 最小: 画面に出す st.title / st.write / st.markdown / st.set_page_config
ex02 入力ウィジェット st.text_input / st.selectbox / st.slider / st.button / st.columns ex01
ex03 状態保持 st.session_state / st.rerun / st.metric / st.expander ex02
ex04 チャット UI(LLM なし) st.chat_message / st.chat_input / st.sidebar ex03
ex05 ストリーミング表示 st.write_stream + ジェネレータ ex04
ex06 OpenAI 統合(同期) OpenAI().chat.completions.create / st.spinner ex04
ex07 OpenAI ストリーミング(最終形) stream=True + st.write_stream ex05 + ex06
ex08 ファイルアップロード st.file_uploader / st.tabs / st.image / st.dataframe ex02

Streamlit の心臓部 — 「再実行(rerun)モデル」

Streamlit を理解する上で最初に必ず腹落ちさせるべき唯一にして最重要の概念。普通の Web フレームワーク(Flask / React)と決定的に違う。

ユーザーが何か操作するたびに、スクリプトが先頭から末尾まで丸ごと再実行される。

ボタンを押す、テキストを打つ、スライダーを動かす — その都度 ex06.pyimport 文から最後の行まで上から下に再実行される。だから「普通のローカル変数」は毎回ゼロから作り直され、前回の値は消える。状態を残したいものだけ st.session_state に逃がす(→ ex03)。

flowchart TD
    A["ブラウザで操作<br>(ボタン/入力/スライダー)"] --> B["スクリプト全体を<br>先頭から再実行"]
    B --> C["session_state を読む<br>(前回の値はここだけ生き残る)"]
    C --> D["st.xxx(...) を順に評価<br>→ 画面要素を再構築"]
    D --> E["ウィジェットの返り値を<br>ローカル変数に受ける"]
    E --> F{操作があった?}
    F -->|button が True 等| G["session_state を更新"]
    G --> A
    F -->|なし| H["待機"]
    H --> A

この図の「session_state だけが再実行をまたいで生き残る」が全サンプルを貫く設計原理。React で言えば、コンポーネント関数が毎レンダリング丸ごと再実行され、useState だけが値を保持するのとそっくり。ただし Streamlit は「再描画の単位=スクリプト全体」なので、依存配列も useEffect もなく、より素朴。


サンプル別の要点

ex01 — 最小構成(st.write は万能関数)

ex01.py / ex01_hello.py。Streamlit の「関数呼び出し=画面要素」を体感する回。

  • st.set_page_config(...)ex01.py:3)は st.xxx の一番最初の呼び出しより前に置く必要がある。後に置くと例外になる(タブのタイトル・アイコンはページ生成の最初に確定させる必要があるため)。
  • st.write(...)ex01.py:6-8)は文字列・数値・dict・DataFrame・Markdown を型を見て勝手に最適な見た目で表示する万能関数。st.write({...}) は dict を折りたためる JSON ビューアにする。
  • st.markdown(...) は Markdown 専用。見出し・リスト・コードブロックを書きたいときはこちらの方が制御しやすい。

ex02 — 入力ウィジェット(「ウィジェットを呼ぶ=値が返る」)

ex02.py / ex02_widgets.py。Streamlit が React や HTML フォームより簡単な核心がここ。

name = st.text_input("あなたの名前", value="ヤマモト")   # ① ユーザーが打った文字列がそのまま返る
submitted = st.button("送信", type="primary")            # ⑧ 押された「その再実行のときだけ」True
if submitted or agree:                                    # ⑨ 返り値を if で受けるのが定石
    ...
やってること なぜそうする
ex02.py:7 text_input の返り値を name に受ける useState 不要。呼び出した時点で最新の入力値が返っている
ex02.py:27 button の返り値を submitted に受ける ボタンは「押された瞬間の1回の再実行だけ True」。次の再実行では False に戻る
ex02.py:31 if submitted or agree: 返り値を if 文で受けないと何も起きない(よくある詰まり所)

ポイント: st.buttonステートレス。「押されたことを覚えておきたい」なら結果を st.session_state に書く(ex03 へ)。st.columns(3)ex03.py:16)は横並びレイアウトを 3 等分で作る。

ex03 — session_state(状態を再実行をまたいで保持)

ex03.py / ex03_session_state.py。rerun モデルの「副作用」を解決する回。カウンターで体感する。

if "counter" not in st.session_state:     # ① 初期化のイディオム:最初の再実行のときだけ作る
    st.session_state.counter = 0
...
if st.button("➕ +1"):
    st.session_state.counter += 1          # 普通の代入で更新できる
    st.session_state.history.append(...)   # list も dict も入れられる
やってること なぜそうする
ex03.py:8-9 if "counter" not in st.session_state: で初期化 毎回再実行されるので、無条件に = 0 すると毎回 0 に戻ってしまう。「無ければ作る」ガードが必須
ex03.py:20 st.session_state.counter += 1 session_state は普通の代入・インクリメントで更新できる(dict-like)
ex03.py:28-30 リセットボタン = 0 を代入。st.rerun() は使っていないが、ボタン押下自体が次の再実行を起こすので表示は更新される
ex03.py:39-40 with st.expander(...)dict(st.session_state) をダンプ デバッグの定番。今の状態を丸ごと覗ける

st.rerun() は「今すぐ先頭から再実行し直せ」という明示命令。ex04 の「履歴クリア後すぐに画面から消したい」場面(ex04.py:36)で使う。

ex04 — チャット UI(履歴は毎回全部描き直す)

ex04.py / ex04_chat_echo.py。LLM なしのエコーボットで、チャット UI の骨格だけを学ぶ。

for msg in st.session_state.messages:          # ② 履歴を毎回 for で全部描き直す
    with st.chat_message(msg["role"]):         #    role に "user"/"assistant" を渡すとバブルの見た目が変わる
        st.markdown(msg["content"])

if user_input := st.chat_input("..."):         # ③ 画面下に固定の入力欄。未入力なら None
    st.session_state.messages.append({"role": "user", "content": user_input})
    ...
やってること なぜそうする
ex04.py:14-16 履歴を for で全描画 rerun モデルでは「差分更新」ではなく毎回ゼロから全部描くのが正しい。React の宣言的レンダリングと同じ発想
ex04.py:19 if user_input := st.chat_input(...) chat_input は送信時だけ文字列、普段は None を返す。walrus 演算子 := で「値があるときだけ処理」を1行で書く
ex04.py:21,27 メッセージを {"role": ..., "content": ...} で append この形は OpenAI / Anthropic API と完全に同じ。ex06 でそのまま LLM に渡せる

履歴データを OpenAI 形式 [{"role","content"}, ...] で持つのが「後で LLM につなぐ」ための布石。

ex05 — write_stream(ジェネレータでタイプライター表示)

ex05.py / ex05_streaming_fake.py。LLM はまだ使わず、ストリーミングの「型」だけ習得する。

def fake_stream(text: str):       # ① yield のたびに1文字を返すジェネレータ
    for char in text:
        yield char
        time.sleep(0.03)

with st.chat_message("assistant"):
    full_response = st.write_stream(fake_stream(response_text))  # ⑥ yield ごとに画面更新、返り値は全結合
st.session_state.stream_messages.append({"role":"assistant","content": full_response})  # ⑦ 結合済みを履歴へ
やってること なぜそうする
ex05.py:10-14 1文字ずつ yield するジェネレータ LLM の token stream を模倣。WebSocket も SSE も自前で書かなくていい
ex05.py:40 full = st.write_stream(gen()) yield ごとに画面追記しつつ、返り値は全 yield を結合した str。これを履歴に保存すれば「ストリーミング後の最終形」が手に入る

st.write_stream の「返り値が結合済み文字列」という性質が ex07 で効く。ストリーム表示と履歴保存を1行で両立できる。

ex06 — OpenAI 統合(同期版・プロンプト実験 UI)

ex06.py / ex06_llm_chat.py。ex04 のオウム返しを本物の LLM に差し替える。

client = OpenAI()                              # ① OPENAI_API_KEY を環境変数から自動取得
api_messages = [
    {"role": "system", "content": system_prompt},  # ⑦ システムプロンプトを先頭に
    *st.session_state.llm_messages,                 #    その後ろに会話履歴をそのまま展開
]
with st.spinner("考え中..."):                   # ⑧ 同期呼び出し中のスピナー表示
    response = client.chat.completions.create(model=model, messages=api_messages, temperature=temperature)
    answer = response.choices[0].message.content
やってること なぜそうする
ex06.py:10 client = OpenAI() キーは引数で渡さず環境変数 OPENAI_API_KEY を自動で読む(load_dotenv().env を読み込み済み)
ex06.py:44-47 [system] + *履歴api_messages を組む ex04 で履歴を OpenAI 形式にしておいたのでそのまま展開して渡せる* でリスト展開
ex06.py:13-21 サイドバーで model / temperature / system_prompt を編集 プロンプト実験 UI のテンプレ。サイドバーの値も再実行のたびに最新が反映される
ex06.py:65-74 response.usage をサイドバーに表示 トークン使用量=コスト把握。同期呼び出しは usage が取れる(ストリーミングだと取りにくい点に注意)

ex07 — OpenAI ストリーミング(最終形・ex05 + ex06 の合体)

ex07.py(= ex07_llm_chat_streaming.py と同一内容)。これで「ChatGPT クローン」が約90行で完成。

def stream_openai(api_messages, model, temperature):
    response_stream = client.chat.completions.create(
        model=model, messages=api_messages, temperature=temperature,
        stream=True,                                  # ← これで返り値がジェネレータになる
    )
    for chunk in response_stream:
        content = chunk.choices[0].delta.content or ""  # 最後の chunk は None なので or "" でガード
        if content:
            yield content                              # str だけを yield(chunk をそのまま渡さない)

with st.chat_message("assistant"):
    full_response = st.write_stream(stream_openai(api_messages, model, temperature))
やってること なぜそうする
ex07.py:66 stream=True OpenAI クライアントの返り値が「1回のレスポンス」から「chunk を吐くジェネレータ」に変わる
ex07.py:70 chunk.choices[0].delta.content or "" ストリーミングは差分 (delta) で届く。終端 chunk は content=None なので or "" でフォールバックしないと st.write_stream に None が渡って壊れる
ex07.py:72 yield content(chunk ではなく str を) st.write_streamstr を yield するジェネレータを期待する。chunk オブジェクトをそのまま yield してはいけない
ex07.py:92 st.write_stream(stream_openai(...)) ex05(write_stream)と ex06(OpenAI)の合流点。返り値の結合文字列をそのまま履歴に保存

ex08 — ファイルアップロード(ex02 の派生)

ex08_file_upload.py のみ。st.tabs で「テキスト / 画像 / CSV / 複数」の4パターンを並べる。連載第22回(領収書 OCR)の前提となる「ファイルを受け取る最小構文」。

やってること なぜそうする
ex08_file_upload.py:22-24 st.tabs([...]) タブで複数パターンを1画面に収める。各タブは with tab_xxx: で囲む
ex08_file_upload.py:33-37 st.file_uploader(type=[...], key=...) type= で拡張子フィルタ。同一画面に複数の uploader を置くなら key= が必須(後述の用語集参照)
ex08_file_upload.py:40 if uploaded is not None: 未アップロード時は None。None ガードが必須
ex08_file_upload.py:42 uploaded.read().decode("utf-8") 返り値 UploadedFile は file-like(.read() / .name / .size / .type)。read() は bytes
ex08_file_upload.py:65 st.image(img, ...) UploadedFilePIL や bytes に変換せず直接渡せる
ex08_file_upload.py:78 pd.read_csv(csv) pandas も file-like を直接受け取る
ex08_file_upload.py:101-105 accept_multiple_files=True 返り値が list[UploadedFile] に変わる。空判定は if files:(None ではなく空リスト)

⚠️ 注意: ex08_file_upload.pyimport pandas as pd を使うが、pyproject.toml の dependencies に pandas が含まれていない(streamlit / openai / python-dotenv のみ)。streamlit が依存で pandas を引き込むため実際には動くことが多いが、CSV タブを確実に動かすなら uv add pandas で明示しておくのが安全。


用語集(最重要)— 混同しやすい同系語を対比で

1. session_state とローカル変数(一番大事な対比)

rerun モデルを理解する核。「何を session_state に逃がし、何をローカル変数のままにするか」で全サンプルの設計が決まる。

ローカル変数(普通の x = ... st.session_state.x
寿命 その1回の再実行だけ。次の操作で消える ブラウザセッション中ずっと生き残る(タブを閉じるまで)
用途 ウィジェットの返り値、その場の計算結果 カウンター・会話履歴・累積する状態
具体例 name = st.text_input(...)(ex02)、api_messages(ex06) st.session_state.counter(ex03)、st.session_state.messages(ex04〜07)
初期化 毎回ゼロから if "x" not in st.session_state:1回だけ

判定基準: 「次にユーザーが操作したとき、この値を覚えていてほしいか?」→ Yes なら session_state、No ならローカル変数。 会話履歴は覚えていてほしい → session_state。今打った1メッセージの文字列は使い切り → ローカル変数(user_input)。

よくある誤解: 「変数に入れたのに次のボタンで消える=バグ」。これはバグではなく rerun モデルの正常動作。ローカル変数は毎回作り直されるのが仕様。残したいなら session_state に入れる。

2. rerun(再実行)と redraw(再描画)

Streamlit には「差分の再描画」という概念がそもそも無い、というのが要点。

用語 意味 具体例
rerun(再実行) スクリプト全体を先頭から実行し直すこと。Streamlit の動作単位はこれ ボタン押下・入力変更で自動発生。st.rerun()(ex04:36)で明示的に起こせる
(存在しない)部分再描画 「このウィジェットだけ更新」のような差分描画 Streamlit には基本ない。毎回全部描き直す(履歴の for ループ=ex04:14 がその証拠)

誤解: 「ボタンを押すとそのボタン周りだけ更新される」→ 違う。スクリプトが丸ごと再実行され、st.xxx が全部また評価されて画面が作り直されるfor msg in messages で毎回全履歴を描くのはこのため。

3. st.button と st.session_state(ステートレス vs ステートフル)

st.button(...) st.checkbox(...) / st.session_state
値の持続 押されたその再実行だけ True、次は False チェック状態は session_state に自動保持され、再実行をまたいで保たれる
性格 ステートレス(イベント的) ステートフル
具体例 submitted = st.button("送信")(ex02:27) agree = st.checkbox(...)(ex02:25)

判定基準: 「押した事実を後の再実行でも覚えたい」なら、button の結果を自分で session_state に書く。 button 単体は1回しか True にならないので、フラグを残したいときは if st.button(...): st.session_state.flag = True とする。

4. write_stream に渡すもの(str を yield ⇄ chunk オブジェクト)

ex07 で一番ハマる対比。

渡してよいもの 渡すと壊れるもの
str を yield するジェネレータyield "あ" OpenAI の chunk オブジェクトをそのまま yield
具体例: fake_stream(ex05)、stream_openai(ex07) for chunk in stream: yield chunk ← NG

stream_openai(ex07:56-72)の仕事は「chunk から delta.content(str)を取り出して yield する変換」。st.write_stream は str を期待するので、この変換を挟まないと表示が壊れる。さらに終端 chunk は content=None なので or "" のガードも必須。

5. message の role 3 種(user / assistant / system)

role どこに出るか どこで使うか
user 右側(ユーザーのバブル)として st.chat_message("user") で描画 履歴に append(ex04:21)
assistant 左側(ボットのバブル) 履歴に append(ex04:27)
system 画面には出さない。LLM への指示専用 API に渡す api_messages の先頭にだけ入れる(ex06:45)

ポイント: system は session_state の履歴には入れない。サイドバーの system_prompt(ローカル変数)として持ち、API 呼び出し時だけ先頭に挿す(ex06:44-47)。履歴に混ぜると画面に出てしまうし、編集も効かなくなる。

6. ウィジェットの key= 引数(同種ウィジェットの衝突回避)

key なし key="..." 指定
何が起きるか 同じ種類・同じ引数のウィジェットが複数あると DuplicateWidgetID エラー 各ウィジェットを一意に識別。session_state にもその key で値が入る
具体例 uploader が1つだけなら省略可 ex08 は uploader を4つ置くので key="text_upload" 等を全部に付与(ex08:36,60,74,104)

判定基準: 同じ種類のウィジェットを1画面に2つ以上置くなら key= 必須。 1つだけなら省略してよい。

7. レイアウト系 API の使い分け

API 役割 具体例
st.sidebar / with st.sidebar: 画面左の固定パネル(設定・操作) model/temperature/履歴クリア(ex06:13)
st.columns(n) 横並びを n 等分 カウンターの +1/-1/リセット(ex03:16)、CSV の表+統計(ex08:83)
st.tabs([...]) タブ切り替え ファイルアップロードの4パターン(ex08:22)
st.expander(...) 折りたたみ session_state ダンプ(ex03:39)
st.spinner(...) 処理中スピナー 同期 LLM 呼び出し中(ex06:51)

困りごと → 見る/使うもの(クイック早見表)

困りごと 見る/使うもの
python ex01.py で起動できない streamlit run ex01.py(専用ランチャーが必要)。README:136
変数の値がボタン押下で消える rerun モデルの正常動作。残したいなら st.session_state に入れる(用語集 1)
ボタンを押しても何も起きない 返り値を if st.button(...): で受ける(用語集 3 / ex02:31)
会話履歴が保持されない st.session_state.messages に append しているか確認(ex04:21)
ストリーミング表示が壊れる st.write_stream に str を yield しているか(chunk 直渡し NG)。終端の or "" ガード(用語集 4 / ex07:70)
システムプロンプトが画面に出てしまう system role は履歴に入れず、API 呼び出し時だけ先頭に挿す(用語集 5 / ex06:44)
DuplicateWidgetID エラー 同種ウィジェットに key= を付ける(用語集 6 / ex08)
ウィジェット操作で毎回再実行されて重い 正常動作。重い処理は @st.cache_data / @st.cache_resource でキャッシュ(README:150)
session_state の中身を確認したい with st.expander(...): st.json(dict(st.session_state))(ex03:39)
トークン使用量を見たい 同期呼び出しの response.usage(ex06:65)。ストリーミングだと取りにくい

学んだこと(要点)

  • rerun モデルが Streamlit のすべて。「操作 → スクリプト全体を先頭から再実行」を腹落ちさせれば、初期化イディオム・履歴の for ループ・session_state の必要性が一本の線でつながる。
  • session_state だけが再実行をまたいで生き残る。React の useState に相当するが、再描画の単位がスクリプト全体なので依存配列も effect もなく素朴。
  • ウィジェットを呼ぶ=最新の入力値が返る。useState も onChange も不要で、これが Streamlit の最速プロトタイピングの源泉。
  • 会話履歴を OpenAI 形式 [{"role","content"}] で持つ布石を ex04 で打っておくと、ex06/07 でそのまま LLM に渡せる。
  • st.write_stream は「str を yield するジェネレータ」契約。fake でも OpenAI でも同じインターフェースで噛み合い、返り値の結合文字列を履歴保存に使える。
  • button はステートレス、checkbox はステートフル。「押した事実を覚えたい」なら自分で session_state に書く。
  • ハマり所は「python で起動」「変数が消える=バグと誤解」「button の返り値を受け忘れる」「write_stream に chunk を直渡し」の4つ。

拡張アイデア

  1. @st.cache_resource で OpenAI クライアントをキャッシュ: ex06/07 は再実行のたびに OpenAI() を作り直している。@st.cache_resource で1回だけ生成するようにし、rerun モデルの無駄を体感する。
  2. 会話履歴を SQLite で永続化: session_state はタブを閉じると消える。ex07 に SQLite 保存・復元を足して「ブラウザを閉じても履歴が残る」を実装し、session_state の寿命の限界を埋める。
  3. ストリーミング中の usage 取得: ex07 はストリーミングなので ex06 のようにトークン使用量が取れない。stream_options={"include_usage": True} を渡して最終 chunk から usage を拾い、サイドバーに出す。
  4. ex08 を ex07 に統合して RAG 風にする: アップロードしたテキストファイルを system プロンプトに注入し、「アップロード文書について答えるボット」にする。連載第22回(領収書 OCR)の縮小版になる。
  5. 複数ページ化(pages/ ディレクトリ): 連載第14回で出てくる multipage 構成にして、ex01〜ex08 を1つのアプリのページとしてまとめる。
  6. st.form でまとめ送信: ex02 は各ウィジェットの変更ごとに再実行されるが、st.form で囲むと「送信ボタンを押すまで再実行しない」挙動になる。rerun の発生タイミングを制御する練習。

既知の注意点

  • ex08_file_upload.pyimport pandas を使うが pyproject.toml の依存に pandas が無い(streamlit 経由で入るため動くことが多いが、明示するなら uv add pandas)。
  • ex07.pyex07_llm_chat_streaming.py は内容がほぼ同一(写経が完成形に到達している)。
  • 写経版(ex0N.py)はコメントが薄いので、原理を確認したいときは注釈つきテンプレート(ex0N_*.py)の docstring と番号コメントを見るのが早い。

記事参照

  • Software Design 連載「実践LLMアプリケーション開発」 第14回(Streamlit 入門)/ 第18回(チャート)/ 第22回(画像アップロード + HITL)/ 第31回(Middleware 可視化)
  • 公式: Streamlit API referencesession_state / chat_message / write_stream / file_uploader

作成: 2026-06-12 / 最終更新: 2026-06-12