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)
- 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). - 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. Jedesst.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() - 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=...)oderst.cache_data(ttl=...), wenn „Aktualisierung“ nur neuere Daten bedeutet. Es gibt KEIN nativesst.autorefresh(verifiziert – nichts indir(st)). Dasstreamlit-autorefreshPyPI-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=...). st.experimental_rerun()existiert nicht (entfernt ab 1.27+). Es heißtst.rerun(). Gleiches gilt fürst.experimental_memo/singleton→st.cache_data/st.cache_resource;st.experimental_fragment→st.fragment;@st.cacheist auch weg.- Die meisten
st.rerun()-Aufrufe sind unnötig. Eine Widget-Interaktion löst bereits eine erneute Ausführung aus, undon_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 einesst.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 dieif "k" not in st.session_state:-Form). Mach es oben, vor den Widgets. - Das Schreiben von
st.session_state.kNACHDEM das Widget mitkey="k"in diesem Durchlauf erstellt wurde, löstStreamlitAPIException: cannot be modifiedaus. 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
keybereits in session_state ist, wirdvalue=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.tabsist 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
StreamlitDuplicateElementIdaus – übergib unterschiedlichekey=-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 WarnungCalling 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 einfachsession_stateund kehre zurück. Das Hinzufügen vonst.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, verifiziertisFalse).ttl="15m"für Daten, die aktualisiert werden sollen;Foo.clear()oderst.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 lasserun_everyes aufgreifen. - Fragmente können
session_statefrei lesen/schreiben; um eine Änderung AUS dem Fragment auf die gesamte Seite zu übertragen, schreibe den Zustand und dannst.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_everywird 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:Bei der vollständigen erneuten Ausführung sendetsecs = 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()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ältdata = ticker()den VERALTETEN Wert und nachgelagerte Diagramme werden nie aktualisiert. Übertrage Live-Daten überst.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.sleepdarin friert die GESAMTE Seite für diese Dauer ein; lassrun_everyden 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 behalterun_everyauf 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_dataLader OHNEttlführt seinen Körper EINMAL aus (verifiziert) und gibt diesen eingefrorenen Wert bei jedem Tick zurück –run_everyfü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 einettl≤ 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 dust.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_datafü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“
- Suche nach
st.rerun– steht jedes hinter einer zuerst umschaltenden Zustandssicherung (Regel 2)? - Suche nach
while True/time.sleepauf Skriptebene → Fragment (Regel 1/3). - Ein Widget schreibt einen Zustandsschlüssel, den eine andere Zeile zurückschreibt → Ping-Pong erneute Ausführung; behalte einen Schreiber pro Schlüssel.
key=-Kollisionen oder F-String-Schlüssel, die sich bei jedem Durchlauf ändern (z.B. mittime.time()) – ein neuer Schlüssel pro Durchlauf = Widget wird bei jedem Durchlauf zurückgesetzt.- Ungecachte langsame Ladevorgänge lassen normale erneute Ausführungen wie Schleifen aussehen – füge
st.cache_dataund einen Ausführungszähler (st.session_state.runs += 1) hinzu, um zu sehen, was tatsächlich passiert. - Live-Bereich aktualisiert sich mit seinem Timer, zeigt aber VERALTETE Werte – eine
@st.cache_data-Funktion ohnettlversorgt ihn; das Fragment wird erneut ausgeführt, aber der Cache gibt den alten Wert zurück. Fügettlhinzu oder cache die Live-Lektüre nicht.