CommunityEscrita e Ediçãogithub.com

st-core

Domine o modelo de execução do Streamlit: estado, reruns, fragmentos, cache e armadilhas comuns.

O que é st-core?

Este skill é um guia de referência essencial para dominar o modelo de execução do Streamlit (versões 1.55+). Ele explica que cada interação do usuário causa uma reexecução completa do script, onde o estado é mantido exclusivamente em `st.session_state`. O documento detalha regras rígidas (como evitar loops infinitos, usar `st.rerun()` com proteção e nunca usar `time.sleep` para autoatualização), além de boas práticas para sessão de estado, formulários, caching e refresh ao vivo com `st.fragment`. Inclui verificações práticas de depuração para loops ou refreshes infinitos, e é válido para todos os principais agentes de IA (Claude Code, Cursor, Codex). O foco principal é evitar os erros clássicos que quebram apps Streamlit e garantir comportamento previsível, performático e sem loops de renderização.

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

Installed? Explore more Escrita e Edição skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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)

  1. 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).
  2. 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. Cada st.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()
    
  3. 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=...), ou st.cache_data(ttl=...) se "refresh" significa apenas dados mais novos. NÃO existe st.autorefresh nativo (verificado — nada em dir(st)). O pacote PyPI streamlit-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=...).
  4. st.experimental_rerun() não existe (removido na 1.27+). É st.rerun(). O mesmo para st.experimental_memo/singleton → st.cache_data / st.cache_resource; st.experimental_fragment → st.fragment; @st.cache também foi removido.
  5. A maioria das chamadas st.rerun() é desnecessária. Uma interação com widget já dispara um rerun, e os callbacks on_click/on_change executam ANTES desse rerun, então suas escritas de estado são visíveis sem pedir outra execução. Usos legítimos: fechar um st.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 forma if "k" not in st.session_state:). Faça no topo, antes dos widgets.
  • Escrever st.session_state.k APÓS o widget com key="k" ter sido criado nesta execução levanta StreamlitAPIException: 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 key já 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 — passe key= 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ário Calling 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 mude session_state e retorne. Adicionar st.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, verificado is False). ttl="15m" para dados que devem ser atualizados; Foo.clear() ou st.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 + deixe run_every pegá-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_every a 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:
    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()
    
    No rerun completo, 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_every ou uma mudança de widget dentro do fragmento, o script externo NÃO é reexecutado, então dados = ticker() mantém o valor DESATUALIZADO e gráficos downstream nunca atualizam. Empurre dados ao vivo via st.session_state escrito 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.sleep dentro dele congela a página INTEIRA por essa duração; deixe run_every ser 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 mantenha run_every no 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_data sem ttl executa seu corpo UMA VEZ (verificado) e retorna aquele valor congelado em cada tick — run_every reexecuta 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 ela ttl ≤ 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ê faz st.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_data na 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"

  1. Pesquise por st.rerun — cada um está atrás de uma proteção de estado que altera primeiro (regra 2)?
  2. Pesquise por while True / time.sleep no nível do script → fragmento (regra 1/3).
  3. Um widget escrevendo uma chave de estado que outra linha também escreve → rerun pingue-pongue; mantenha um escritor por chave.
  4. Colisões de key= ou chaves de f-string que mudam a cada execução (ex.: contendo time.time()) — uma nova chave a cada execução = widget reinicia a cada execução.
  5. Cargas lentas sem cache fazem reruns normais PARECEREM loops — adicione st.cache_data e um contador de execuções (st.session_state.execucoes += 1) para ver o que realmente está acontecendo.
  6. Seção ao vivo atualiza no seu timer mas mostra valores DESATUALIZADOS → uma função @st.cache_data sem ttl a alimenta; o fragmento reexecuta mas o cache retorna o valor antigo. Adicione ttl ou não coloque em cache a leitura ao vivo.

Habilidades Relacionadas