CommunitySchreiben & Editierengithub.com

Streamlit-Kern (1.58.x) – Ausführungsmodell, Zustand, Aktualisi…

Ein Skill, der das typische Streamlit-Problem löst: unendliche Loops, verschwindende Ausgaben und veraltete Daten. Er erklärt das wichtige konzeptionelle Modell und bietet klare Regeln für die Arbeit mit session_state, Callbacks, Caching und Live-Aktualisierung.

Was ist Streamlit-Kern (1.58.x) – Ausführungsmodell, Zustand, Aktualisi…?

Dieser Skill beschreibt das grundlegende Ausführungsmodell von Streamlit: Das gesamte Skript wird bei jeder Benutzerinteraktion von oben nach unten neu ausgeführt (eine Wiederholung ist eine reine Funktion von `st.session_state` + aktuellen Widget-Werten). Er erklärt die wichtigsten Regeln, um die häufigsten Fehler zu vermeiden: Verwenden Sie NIEMALS `while True` auf Skriptebene (verwenden Sie stattdessen `@st.fragment(run_every=...)`), rufen Sie `st.rerun()` nicht ohne Zustandsschutz auf, und verwenden Sie nicht `time.sleep; st.rerun()` für Live-Aktualisierung. Stattdessen wird `@st.fragment(run_every=...)` empfohlen. Der Skill behandelt die korrekte Verwendung von session_state (Schreiben nach der Widget-Erstellung vermeiden, Callbacks zum Setzen von Zuständen nutzen), Formulare (keine Wiederholung bei jedem Tastendruck), Caching (`@st.cache_data` und `@st.cache_resource` mit ttl) und das Fragment-Muster für Live-Daten ohne vollständige Seitenwiederholung. Er enthält auch eine Debug-Checkliste für hängende oder sich wiederholende Apps. Der Skill wurde für Streamlit 1.58 entwickelt und auf 1.55 getestet und ist im Kontext des `4k7yvjv7w4-ops/streamlit-agent-skills` Repositorys verfügbar.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add 4k7yvjv7w4-ops/streamlit-agent-skills

Installed? Explore more Schreiben & Editieren skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Streamlit-Kern (1.58.x) – Ausführungsmodell, Zustand, Aktualisierung

Das eine mentale Modell: Bei JEDER Benutzerinteraktion (jedes Widget berührt) führt Streamlit das GESAMTE Skript von oben nach unten neu aus. Es gibt keine Event-Handler, keine persistenten lokalen Variablen, keine Hauptschleife. Ein Durchlauf ist eine reine Funktion von st.session_state + aktuellen Widget-Werten. Alles Weitere folgt daraus.

Überprüfbarer Beweis jeder Behauptung: st_core_lab.py in diesem Skill-Ordner (python -m streamlit run ~/.roo/skills/st-core/st_core_lab.py). Verifiziert auf Streamlit 1.58, API-geprüft auf 1.55: Alles in diesem Skill existiert auf beiden Versionen (st.fragment(run_every=) benötigt ≥1.37).

Harte Regeln (Verstöße = die klassische kaputte App)

  1. NIE while True: auf Skriptebene. Das Skript muss jeden Durchlauf beenden. Verwende für einen Live-Bereich @st.fragment(run_every="2s") (siehe unten). Schleifen sind nur INNERHALB eines Durchlaufs für eine endliche Aufgabe erlaubt (Fortschrittsbalken, st.write_stream).
  2. NIE st.rerun() bedingungslos aufrufen (oder unter einer Bedingung, die beim nächsten Durchlauf immer noch wahr ist) – Streamlit hat KEINEN Schleifenschutz; es dreht sich ewig weiter. Jedes st.rerun() muss hinter einer Zustandssicherung stehen, die du zuerst umschaltest:
    if st.session_state.get("needs_refresh"):
        st.session_state.needs_refresh = False   # VOR rerun umschalten
        refresh_data()
        st.rerun()
    
  3. NIE time.sleep(n); st.rerun() für automatische Aktualisierung. Die Seite beruhigt sich nie: Widgets flackern, Klicks werden während des Schlafens verschluckt. Verwende @st.fragment(run_every=...) oder st.cache_data(ttl=...), wenn „Aktualisierung“ nur neuere Daten bedeutet. Es gibt KEIN natives st.autorefresh (verifiziert – nichts in dir(st)). Das streamlit-autorefresh PyPI-Paket ist von Drittanbietern und führt bei jedem Tick das GESAMTE Skript neu aus (setzt jedes Widget zurück, stiehlt den Fokus); bevorzuge das native, bereichsbezogene @st.fragment(run_every=...).
  4. st.experimental_rerun() existiert nicht (entfernt ab 1.27+). Es heißt st.rerun(). Gleiches gilt für st.experimental_memo/singleton → st.cache_data / st.cache_resource; st.experimental_fragment → st.fragment; @st.cache ist auch weg.
  5. Die meisten st.rerun()-Aufrufe sind unnötig. Eine Widget-Interaktion löst bereits eine erneute Ausführung aus, und on_click/on_change-Callbacks werden VOR dieser erneuten Ausführung ausgeführt, sodass ihre Zustandsschreibungen sichtbar sind, ohne eine weitere anzufordern. Legitime Verwendungen: Schließen eines st.dialog, Anwenden einer programmatischen Zustandsänderung, die UNTERHALB der Stelle vorgenommen wurde, an der sie gerendert wird (Regel-2-Muster).

st.button ist momentan – der häufigste Bug mit verschwindender Ausgabe

st.button() gibt für GENAU EINEN Durchlauf (den Klick) True zurück, dann wieder False. Wenn also if st.button("run"): st.write(result) das Ergebnis anzeigt, bis der Benutzer etwas anderes berührt, dann VERSCHWINDET es. Und verschachtelte Buttons können nie beide wahr sein: Das Klicken auf den inneren macht den äußeren wieder False (verifiziert).

# FALSCH                              # RICHTIG – mit Zustand persistieren
if st.button("berechnen"):            if st.button("berechnen"):
    r = model()                           st.session_state.r = model()
    st.write(r)   # weg n. nächstem Run    if "r" in st.session_state:
                                              st.write(st.session_state.r)

Mehrschrittige Abläufe: Ein step-Schlüssel in session_state + st.rerun() gemäß Regel 2, niemals Button-in-Button.

session_state-Regeln

  • Einmal initialisieren: st.session_state.setdefault("k", Standardwert) (oder die if "k" not in st.session_state:-Form). Mach es oben, vor den Widgets.
  • Das Schreiben von st.session_state.k NACHDEM das Widget mit key="k" in diesem Durchlauf erstellt wurde, löst StreamlitAPIException: cannot be modified aus. Um ein Widget programmatisch zu setzen, schreibe den Schlüssel VOR die Widget-Zeile (normalerweise: setze ein Flag + st.rerun(), wende das Schreiben am Anfang des nächsten Durchlaufs an) – oder mache es in einem Callback, das ist immer sicher.
  • Wenn key bereits in session_state ist, wird value= ignoriert – der Zustand gewinnt; die einzige Spur ist eine Warnung im SERVER-Log ("created with a default value but also had its value set via the Session State API"), nichts in der UI. Übergib nicht beides und erwarte, dass der Standardwert angewendet wird.
  • Ein Widget, dessen Zeile in einem Durchlauf NICHT ausgeführt wird, verliert seinen Zustand (versteckter Tab-by-Bedingung, collapsed branch – verifiziert: Slider bei 8 kam bei 0 zurück). Um das Überleben beim Ausblenden zu gewährleisten, spiegele es in einen eigenen Schlüssel: st.slider(..., key="_w", on_change=lambda: st.session_state.update(w=st.session_state._w)) und speise es über das Schreiben-vor-Widget-Muster zurück. (st.tabs ist sicher – der Code aller Tabs wird jedes Mal ausgeführt, ob versteckt oder nicht; deshalb benötigen schwere Tabs Caching.)
  • Zwei Widgets mit identischem Typ + Parametern lösen StreamlitDuplicateElementId aus – übergib unterschiedliche key=-Werte. Widgets in Schleifen: key=f"row-{i}".

Callbacks schlagen manuelle erneute Ausführungen

Ein Zustand, der unterhalb seiner Rendering-Stelle mutiert wird, liest für einen Durchlauf veraltet (verifiziert: Der if st.button(...)-Zweig wird AUSGEFÜHRT, nachdem der Zähler darüber gezeichnet wurde). Callbacks beheben die Reihenfolge – sie werden ausgeführt, bevor das Skript erneut ausgeführt wird:

st.session_state.setdefault("count", 0)
def inc(): st.session_state.count += 1
st.button("+1", on_click=inc)                 # count ist in diesem selben Durchlauf frisch
st.metric("count", st.session_state.count)

on_change bei Eingaben/Auswahlen funktioniert genauso. Callbacks können jeden Zustandsschlüssel schreiben (sogar den eines Widgets, da seine Zeile noch nicht ausgeführt wurde). Kein st.rerun() nötig.

  • st.rerun() innerhalb eines Callbacks ist ein NO-OP (verifiziert): Es zeigt eine für den Benutzer sichtbare Warnung Calling st.rerun() within a callback is a no-op. und BRICHT den Rest des Callback-Bodys AB – keine zusätzliche erneute Ausführung, keine Ausnahme. Der Klick hat die erneute Ausführung bereits geplant; mutiere in einem Callback einfach session_state und kehre zurück. Das Hinzufügen von st.rerun() dort, um die Änderung anzuwenden, schneidet deinen Callback stillschweigend ab.

Formulare – Stopp der Wiederholung pro Tastendruck

Jede Widget-Interaktion führt das Skript erneut aus; N Eingaben = N erneute Ausführungen, während der Benutzer sie ausfüllt (jede trifft erneut deine Datenebene). Bündle sie:

with st.form("params"):
    a = st.slider("a", 0, 10)
    b = st.selectbox("b", optionen)
    go = st.form_submit_button("Anwenden")
if go: run_query(a, b)

Nichts wird erneut ausgeführt, bis zum Absenden (verifiziert: Ausführungszähler +1 insgesamt). Innerhalb eines Formulars: kein st.button, keine Callbacks pro Widget, außer auf dem Submit-Button.

"Es wird während der Eingabe neu ausgeführt" liegt fast nie an Tastendrücken. Widgets übergeben bei verschiedenen Ereignissen (Streamlit Frontend-Verhalten): text_input/text_area/number_input feuern bei Enter oder Fokusverlust, slider/select_slider beim Loslassen – NICHT pro Tastendruck, NICHT während des Ziehens; checkbox/radio/selectbox/toggle feuern sofort. Eine geänderte Übergabe = eine erneute Ausführung, und das Frontend sendet nichts, wenn der Wert unverändert ist. Ständige erneute Ausführungen während der Eingabe bedeuten also eine gleichzeitige automatische Aktualisierung der gesamten App (ein Timer / st_autorefresh / sleep+rerun), die den Fokus stiehlt – begrenze diese Aktualisierung auf ein @st.fragment, das die Eingabe NICHT enthält, oder umschließe die Eingaben mit st.form. Entprelle keine Tastendrücke; Streamlit braucht das nicht.

Caching – die meiste „schlechte Aktualisierung“ ist eigentlich das hier

  • @st.cache_data – DataFrames/serialisierbare Ergebnisse. Gibt pro Aufruf eine FRISCHE KOPIE zurück (Mutationen sicher, verifiziert is False). ttl="15m" für Daten, die aktualisiert werden sollen; Foo.clear() oder st.cache_data.clear() auf einem ↻-Button.
  • @st.cache_resource – Verbindungen, Modelle, Threads. SELBES Objekt, das an alle Sitzungen zurückgegeben wird (verifiziert) – mutiere es nie pro Benutzer, füge bei Bedarf Sperren hinzu.
  • Argumente müssen hashbar sein und bilden den Cache-Schlüssel; stelle unhashbaren ein _ voran, um sie auszuschließen (def load(_conn, day):).
  • Ohne Caching führt jeder Widget-Tick jede Ladung im Skript erneut aus – die App „fühlt sich eingefroren an“, weil jeder Klick Sekunden kostet. Cache zuerst, dann debugge erneute Ausführungen.

Live-Aktualisierung richtig gemacht – st.fragment

@st.fragment(run_every="2s")        # int Sekunden / timedelta / "2s"-Strings
def ticker():
    st.metric("letzter", get_price())  # nur DIESE Funktion wird erneut ausgeführt
ticker()
  • Der Rest der Seite wird NICHT erneut ausgeführt (verifiziert: äußerer Ausführungszähler eingefroren, während das Fragment tickt). Widgets innerhalb des Fragments führen nur das Fragment erneut aus; st.rerun(scope="fragment") startet nur das Fragment neu.
  • Achtung (verifiziert): scope="fragment" WIRD GEWORFEN, wenn der Fragmentkörper als Teil eines vollständigen App-Durchlaufs ausgeführt wird (erster Paint) – schütze es mit einer Bedingung oder schalte den Zustand um und lasse run_every es aufgreifen.
  • Fragmente können session_state frei lesen/schreiben; um eine Änderung AUS dem Fragment auf die gesamte Seite zu übertragen, schreibe den Zustand und dann st.rerun() (Scope "app", der Standard).
  • Stoppe oder ändere die Zeit eines run_every-Fragments vom ÄUSSEREN Skript aus – ein Widget INNERHALB kann das nicht. run_every wird nur bei einem VOLLSTÄNDIGEN App-Durchlauf neu gelesen, daher ein Intervall-Schieberegler oder ein Stopp-Button innerhalb des Fragments (das nur das Fragment erneut ausführt) aktiviert den Timer nie neu (verifiziert). Schalte den Fragment-AUFRUF von außen:
    secs = st.slider("interval", 1, 10, 2)          # AUSSERHALB des Fragments
    run  = st.session_state.get("run", True)
    tick = st.fragment(run_every=secs if run else None)(_body)
    tick()
    if st.button("stop"): st.session_state.run = False; st.rerun()
    
    Bei der vollständigen erneuten Ausführung sendet run_every=None (oder der Aufruf wird nicht ausgeführt) keine automatische Aktualisierungsnachricht und der Timer stoppt.
  • Der Rückgabewert eines Fragments erreicht den Aufrufer nur bei einem VOLLSTÄNDIGEN Durchlauf (verifiziert). Bei einem run_every-Tick oder einer Widget-Änderung innerhalb des Fragments wird das äußere Skript NICHT erneut ausgeführt, daher behält data = ticker() den VERALTETEN Wert und nachgelagerte Diagramme werden nie aktualisiert. Übertrage Live-Daten über st.session_state, das INNERHALB des Fragments geschrieben wird (st.session_state.data = fetch()), lies es außerhalb – oder zeichne das Diagramm innerhalb des Fragments selbst. Behandle ein Fragment nicht als einfache Funktion, der du über mehrere Ticks hinweg einen Wert zuweisen kannst.
  • Ein Fragment läuft im einzigen Thread der App – es ist KEIN Hintergrundthread. time.sleep darin friert die GESAMTE Seite für diese Dauer ein; lass run_every den Rhythmus vorgeben, schlafe nie. Verschachtelte Fragmente sind erlaubt (verifiziert), aber jedes ist ein unabhängiger Wiederholungsbereich – ein untergeordneter Tick führt nicht das übergeordnete Fragment erneut aus, also behalte run_every auf dem Fragment, das die Daten besitzt; verschachtele keine Timer für dieselben Daten. Für echte Hintergrundarbeit (einen langsamen Job absenden, weiter navigieren, Ergebnisse treffen ein) → [st-jobs].
  • Erstelle kein Widget mit demselben Schlüssel sowohl innerhalb als auch außerhalb eines Fragments.
  • Fragment tickt, aber der Wert ändert sich nie? Deine Live-Lektüre ist gecached. Ein @st.cache_data Lader OHNE ttl führt seinen Körper EINMAL aus (verifiziert) und gibt diesen eingefrorenen Wert bei jedem Tick zurück – run_every führt die Funktion erneut aus, aber der Cache unterbricht sie. Dies ist die häufigste Ursache für „mein Fragment aktualisiert sich nicht“. Behebung: Cache die Live-Lektüre nicht, ODER gib ihr eine ttl ≤ dem Aktualisierungsrhythmus (@st.cache_data(ttl="2s")), ODER variiere ein Argument bei jedem Tick.

Fragment als bereichsbezogener Arbeitskasten – jetzt abrufen, später verwenden (keine vollständige erneute Ausführung)

Ein Fragment OHNE run_every ist ein kastenförmiger Bereich auf der Seite, dessen Widgets NUR den Kasten erneut ausführen – das Muster für „Referenzdaten hier nachschlagen, in einer späteren Aktion konsumieren“, ohne die gesamte Seite pro Nachschlagevorgang erneut auszuführen:

@st.fragment
def fetch_box():                              # Klicks hier führen NUR den Kasten erneut aus
    with st.form("lookup"):                   # Formular innerhalb eines Fragments: legal (verifiziert)
        code = st.selectbox("Entität", codes, key="box_code")
        go = st.form_submit_button("Details abrufen")
    if go:
        st.session_state.details = get_details(code)   # Übergabe über STATUS
        st.success(f"{code} abgerufen")
fetch_box()

if st.button("Anfrage ausführen"):                  # AUSSERHALB → sein Klick IST eine vollständige erneute Ausführung,
    d = st.session_state.get("details")       # welche die gespeicherten Details liest
    if d is None: st.warning("zuerst abrufen")
    else:        run_request(d)
  • Der Konsument braucht kein st.rerun(): Sein eigener Klick ist ein vollständiger Durchlauf, der den frischen Zustand liest (verifiziert). Nur wenn die ÄUSSERE Seite die abgerufenen Daten SOFORT anzeigen muss (vor einem weiteren Klick), führst du st.rerun() innerhalb des Fragments aus, nachdem du den Zustand geschrieben hast – kostet einen vollständigen Durchlauf.
  • Übergabe über session_state, niemals über den Rückgabewert des Fragments (veraltet bei kasteninternen erneuten Ausführungen – siehe oben). Verwende @st.cache_data für die Suche, sodass das erneute Abrufen derselben Entität kostenlos ist.

Status während der Arbeit (innerhalb EINES Durchlaufs – erlaubte Schleifen)

ph = st.empty()                       # Platzhalter, an Ort und Stelle überschreiben
for i, step in enumerate(schritte):
    ph.text(f"{i+1}/{len(schritte)} {step.name}")
    step.run()
ph.empty()

Auch st.progress, st.status("...", expanded=True), st.spinner, st.toast. Diese animieren INNERHALB eines Durchlaufs – keine erneute Ausführung, kein Schleifenrisiko.

Debug-Checkliste für „steckt in einer Schleife / aktualisiert ständig“

  1. Suche nach st.rerun – steht jedes hinter einer zuerst umschaltenden Zustandssicherung (Regel 2)?
  2. Suche nach while True / time.sleep auf Skriptebene → Fragment (Regel 1/3).
  3. Ein Widget schreibt einen Zustandsschlüssel, den eine andere Zeile zurückschreibt → Ping-Pong erneute Ausführung; behalte einen Schreiber pro Schlüssel.
  4. key=-Kollisionen oder F-String-Schlüssel, die sich bei jedem Durchlauf ändern (z.B. mit time.time()) – ein neuer Schlüssel pro Durchlauf = Widget wird bei jedem Durchlauf zurückgesetzt.
  5. Ungecachte langsame Ladevorgänge lassen normale erneute Ausführungen wie Schleifen aussehen – füge st.cache_data und einen Ausführungszähler (st.session_state.runs += 1) hinzu, um zu sehen, was tatsächlich passiert.
  6. Live-Bereich aktualisiert sich mit seinem Timer, zeigt aber VERALTETE Werte – eine @st.cache_data-Funktion ohne ttl versorgt ihn; das Fragment wird erneut ausgeführt, aber der Cache gibt den alten Wert zurück. Füge ttl hinzu oder cache die Live-Lektüre nicht.

Verwandte Skills