Streamlit core (1.58.x) — modelo de execução, estado, refresh
O único modelo mental: em CADA interação do usuário (qualquer widget tocado),
o Streamlit reexecuta o script INTEIRO de cima a baixo. Não há
manipuladores de eventos, variáveis locais persistentes ou loop principal.
Uma execução é uma função pura de st.session_state + valores atuais dos widgets.
Tudo abaixo decorre disso.
Prova executável de cada afirmação: st_core_lab.py nesta pasta de skill
(python -m streamlit run ~/.roo/skills/st-core/st_core_lab.py).
Verificado no Streamlit 1.58, API verificada na 1.55: tudo neste
skill existe em ambas (st.fragment(run_every=) requer ≥1.37).
Regras rígidas (violações = o app quebrado clássico)
- NUNCA
while True:no nível do script. O script deve finalizar cada execução. Para uma seção ao vivo use@st.fragment(run_every="2s")(abaixo). Loops são permitidos apenas DENTRO de uma execução para um trabalho finito (barra de progresso,st.write_stream). - NUNCA chame
st.rerun()incondicionalmente (ou sob uma condição que ainda seja verdadeira na próxima execução) — o Streamlit NÃO tem proteção contra loop; ele gira para sempre. Cadast.rerun()deve estar atrás de uma proteção de estado que você altera primeiro:if st.session_state.get("needs_refresh"): st.session_state.needs_refresh = False # alterar ANTES do rerun refresh_data() st.rerun() - NUNCA
time.sleep(n); st.rerun()para atualização automática. A página nunca se estabiliza: os widgets piscam, os cliques são engolidos durante o sono. Use@st.fragment(run_every=...), oust.cache_data(ttl=...)se "refresh" significa apenas dados mais novos. NÃO existest.autorefreshnativo (verificado — nada emdir(st)). O pacote PyPIstreamlit-autorefreshé de terceiros e reexecuta o script INTEIRO a cada tick (reinicia todos os widgets, rouba o foco); prefira o nativo com escopo@st.fragment(run_every=...). st.experimental_rerun()não existe (removido na 1.27+). Ést.rerun(). O mesmo parast.experimental_memo/singleton→st.cache_data/st.cache_resource;st.experimental_fragment→st.fragment;@st.cachetambém foi removido.- A maioria das chamadas
st.rerun()é desnecessária. Uma interação com widget já dispara um rerun, e os callbackson_click/on_changeexecutam ANTES desse rerun, então suas escritas de estado são visíveis sem pedir outra execução. Usos legítimos: fechar umst.dialog, aplicar uma mudança de estado programática feita ABAIXO de onde é renderizada (padrão regra 2).
st.button é momentâneo — o bug nº1 de saída que desaparece
st.button() retorna True por EXATAMENTE uma execução (o clique), depois False
novamente. Então if st.button("executar"): st.write(resultado) mostra o resultado até que o
usuário toque em qualquer outra coisa, e então ele DESAPARECE. E botões aninhados nunca podem
ambos ser verdadeiros: clicar no interno faz o externo voltar a False (verificado).
# ERRADO # CERTO — persistir via estado
if st.button("calcular"): if st.button("calcular"):
r = modelo() st.session_state.r = modelo()
st.write(r) # some na prox exec if "r" in st.session_state:
st.write(st.session_state.r)
Fluxos de várias etapas: uma chave step no session_state + st.rerun() conforme regra 2,
nunca botão dentro de botão.
Regras de session_state
- Inicializar uma vez:
st.session_state.setdefault("k", padrao)(ou a formaif "k" not in st.session_state:). Faça no topo, antes dos widgets. - Escrever
st.session_state.kAPÓS o widget comkey="k"ter sido criado nesta execução levantaStreamlitAPIException: cannot be modified. Para definir um widget programaticamente, escreva a chave ANTES da linha do widget (geralmente: definir uma flag +st.rerun(), aplicar a escrita no topo da próxima execução) — ou faça em um callback, que é sempre seguro. - Se
keyjá está em session_state,value=é ignorado — o estado vence; o único vestígio é um aviso no log do SERVIDOR ("created with a default value but also had its value set via the Session State API"), nada na interface. Não passe ambos e espere que o padrão seja aplicado. - Um widget cuja linha não executa em uma execução PERDE seu estado (aba
oculta por condição, ramo colapsado — verificado: slider em 8 voltou para 0).
Para sobreviver à ocultação, espelhe para sua própria chave:
st.slider(..., key="_w", on_change=lambda: st.session_state.update(w=st.session_state._w))e alimente de volta via o padrão escreva-antes-do-widget. (st.tabsé seguro — o código de todas as abas executa toda vez, ocultas ou não; é por isso que abas pesadas precisam de cache.) - Dois widgets com tipo+params idênticos levantam
StreamlitDuplicateElementId— passekey=distintos. Widgets em loops:key=f"linha-{i}".
Callbacks vencem reruns manuais
Estado mutado abaixo de seu ponto de renderização fica desatualizado por uma execução (verificado:
o ramo if st.button(...) executa APÓS o contador acima dele ter sido desenhado).
Callbacks corrigem a ordenação — eles executam antes da reexecução do script:
st.session_state.setdefault("count", 0)
def inc(): st.session_state.count += 1
st.button("+1", on_click=inc) # count está fresco nesta mesma execução
st.metric("count", st.session_state.count)
on_change em inputs/selects funciona da mesma forma. Callbacks podem escrever qualquer chave de estado
(mesmo a de um widget, já que sua linha ainda não executou). Nenhum st.rerun() necessário.
st.rerun()dentro de um callback é um NO-OP (verificado): mostra um aviso visível ao usuárioCalling st.rerun() within a callback is a no-op.e ABORTA o resto do corpo do callback — sem rerun extra, sem exceção. O clique já agendou o rerun; em um callback apenas mudesession_statee retorne. Adicionarst.rerun()ali "para aplicar a mudança" silenciosamente trunca seu callback.
Formulários — pare o rerun por tecla pressionada
Cada interação com widget reexecuta o script; N inputs = N reruns enquanto o usuário os preenche (cada um reatingindo sua camada de dados). Agrupe-os:
with st.form("params"):
a = st.slider("a", 0, 10)
b = st.selectbox("b", opcoes)
go = st.form_submit_button("Aplicar")
if go: executar_consulta(a, b)
Nada reexecuta até o envio (verificado: contador de execuções +1 no total). Dentro de um formulário:
nenhum st.button, nenhum callback por widget exceto no botão de envio.
"Ele reexecuta enquanto digito" quase nunca são teclas. Widgets confirmam em
eventos diferentes (comportamento do frontend Streamlit): text_input/text_area/
number_input disparam ao pressionar Enter ou ao perder o foco, slider/select_slider ao
soltar — NÃO por tecla, NÃO durante o arrasto; checkbox/radio/selectbox/
toggle disparam imediatamente. Uma mudança confirmada = um rerun, e o frontend
não envia nada quando o valor não mudou. Então reruns constantes enquanto digita significam
uma concorrente atualização automática de todo o app (um timer / st_autorefresh / sleep+
rerun) roubando o foco — isole esse refresh em um @st.fragment que NÃO
contenha o input, ou envolva os inputs em st.form. Não faça debounce de teclas;
Streamlit não precisa disso.
Cache — a maioria dos "refresh ruim" é na verdade isso
@st.cache_data— DataFrames/resultados serializáveis. Retorna uma CÓPIA fresca por chamada (mutações seguras, verificadoisFalse).ttl="15m"para dados que devem ser atualizados;Foo.clear()oust.cache_data.clear()em um botão ↻.@st.cache_resource— conexões, modelos, threads. O MESMO objeto retornado para todas as sessões (verificado) — nunca o mude por usuário, adicione locks se necessário.- Os argumentos devem ser hashable e formam a chave do cache; prefixe os não hashable com
_para excluí-los (def carregar(_conn, dia):). - Sem cache, cada tick de widget reexecuta cada carregamento no script — o app "parece travado" porque cada clique custa segundos. Cache primeiro, depois depure reruns.
Refresh ao vivo feito certo — st.fragment
@st.fragment(run_every="2s") # int segundos / timedelta / strings "2s"
def ticker():
st.metric("último", get_price()) # apenas ESTA função reexecuta
ticker()
- O resto da página NÃO reexecuta (verificado: contador de execuções externo congelado
enquanto o fragmento ticka). Widgets dentro do fragmento reexecutam apenas o
fragmento;
st.rerun(scope="fragment")reinicia apenas ele. - Pegadinha (verificado):
scope="fragment"LEVANTA exceção se o corpo do fragmento está executando como parte de uma execução completa do app (primeira renderização) — proteja-o, ou alterne estado + deixerun_everypegá-lo. - Fragmentos podem ler/escrever session_state livremente; para empurrar uma mudança
PARA FORA para a página inteira, escreva estado e então
st.rerun()(escopo"app", o padrão). - Parar ou re-temporizar um fragmento
run_everya partir do script EXTERNO — um widget DENTRO dele não pode.run_everyé relido apenas em uma execução COMPLETA do app, então um slider de intervalo ou botão Parar dentro do fragmento (que apenas reexecuta o fragmento) nunca rearma o timer (verificado). Controle a CHAMADA do fragmento de fora:No rerun completo,segs = st.slider("intervalo", 1, 10, 2) # FORA do fragmento exec = st.session_state.get("exec", True) tick = st.fragment(run_every=segs if exec else None)(_corpo) tick() if st.button("parar"): st.session_state.exec = False; st.rerun()run_every=None(ou não chamá-lo) não emite mensagem de auto-refresh e o timer para. - O valor de retorno de um fragmento só atinge o chamador em uma execução COMPLETA (verificado).
Em um tick
run_everyou uma mudança de widget dentro do fragmento, o script externo NÃO é reexecutado, entãodados = ticker()mantém o valor DESATUALIZADO e gráficos downstream nunca atualizam. Empurre dados ao vivo viast.session_stateescrito DENTRO do fragmento (st.session_state.dados = buscar()), leia fora — ou desenhe o gráfico dentro do próprio fragmento. Não trate um fragmento como uma função simples que você pode atribuir entre ticks. - Um fragmento executa na thread única do app — NÃO é uma thread em segundo plano.
time.sleepdentro dele congela a página INTEIRA por essa duração; deixerun_everyser a cadência, nunca durma. Fragmentos aninhados são permitidos (verificado), mas cada um é um escopo de rerun independente — um tick filho não reexecuta seu pai, então mantenharun_everyno fragmento que possui os dados; não aninhe timers para os mesmos dados. Para trabalho REAL em segundo plano (enviar um trabalho lento, continuar navegando, resultados chegam) → [st-jobs]. - Não crie o mesmo widget com a mesma chave tanto dentro quanto fora de um fragmento.
- O fragmento ticka mas o valor nunca muda? Sua leitura ao vivo está em cache.
Um carregador
@st.cache_datasemttlexecuta seu corpo UMA VEZ (verificado) e retorna aquele valor congelado em cada tick —run_everyreexecuta a função mas o cache a encurta. Esta é a causa nº1 de "meu fragmento não atualiza". Correção: não coloque em cache a leitura ao vivo, OU dê a elattl≤ a cadência de refresh (@st.cache_data(ttl="2s")), OU varie um argumento a cada tick.
Fragmento como caixa de trabalho com escopo — buscar agora, usar depois (sem rerun completo)
Um fragmento SEM run_every é uma caixa na página cujos widgets reexecutam APENAS a
caixa — o padrão para "consultar dados de referência aqui, consumir em uma ação posterior"
sem reexecutar a página inteira por consulta:
@st.fragment
def caixa_busca(): # cliques aqui reexecutam APENAS a caixa
with st.form("consulta"): # formulário dentro do fragmento: legal (verificado)
cod = st.selectbox("entidade", codigos, key="box_cod")
go = st.form_submit_button("buscar detalhes")
if go:
st.session_state.detalhes = get_details(cod) # entregar via ESTADO
st.success(f"buscado {cod}")
caixa_busca()
if st.button("executar requisição"): # FORA → seu clique É um rerun completo,
d = st.session_state.get("detalhes") # que lê os detalhes armazenados
if d is None: st.warning("busque primeiro")
else: executar_requisicao(d)
- O consumidor NÃO precisa de
st.rerun(): seu próprio clique é uma execução completa que lê estado fresco (verificado). Apenas se a página EXTERNA precisar exibir os dados buscados IMEDIATAMENTE (antes de qualquer próximo clique) você fazst.rerun()dentro do fragmento após escrever o estado — custa uma execução completa. - Entregue via
session_state, nunca pelo valor de retorno do fragmento (desatualizado entre reruns apenas da caixa — bala acima). Use@st.cache_datana consulta para que buscar a mesma entidade novamente seja gratuito.
Status enquanto trabalha (dentro de UMA execução — loops permitidos)
ph = st.empty() # placeholder, sobrescreve no lugar
for i, passo in enumerate(passos):
ph.text(f"{i+1}/{len(passos)} {passo.nome}")
passo.executar()
ph.empty()
Também st.progress, st.status("...", expanded=True), st.spinner,
st.toast. Estes animam DENTRO de uma execução — sem rerun envolvido, sem risco de loop.
Checklist de depuração para "preso em um loop / fica atualizando"
- Pesquise por
st.rerun— cada um está atrás de uma proteção de estado que altera primeiro (regra 2)? - Pesquise por
while True/time.sleepno nível do script → fragmento (regra 1/3). - Um widget escrevendo uma chave de estado que outra linha também escreve → rerun pingue-pongue; mantenha um escritor por chave.
- Colisões de
key=ou chaves de f-string que mudam a cada execução (ex.: contendotime.time()) — uma nova chave a cada execução = widget reinicia a cada execução. - Cargas lentas sem cache fazem reruns normais PARECEREM loops — adicione
st.cache_datae um contador de execuções (st.session_state.execucoes += 1) para ver o que realmente está acontecendo. - Seção ao vivo atualiza no seu timer mas mostra valores DESATUALIZADOS → uma
função
@st.cache_datasemttla alimenta; o fragmento reexecuta mas o cache retorna o valor antigo. Adicionettlou não coloque em cache a leitura ao vivo.