<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jean Hoccart</title>
    <description>The latest articles on DEV Community by Jean Hoccart (@jean_hoccart_0e38d2242cd2).</description>
    <link>https://dev.to/jean_hoccart_0e38d2242cd2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172107%2Fd9d9f867-a552-4221-9704-51abb4f3420f.png</url>
      <title>DEV Community: Jean Hoccart</title>
      <link>https://dev.to/jean_hoccart_0e38d2242cd2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jean_hoccart_0e38d2242cd2"/>
    <language>en</language>
    <item>
      <title>How a server-driven Python framework decides which part of the page to re-render</title>
      <dc:creator>Jean Hoccart</dc:creator>
      <pubDate>Thu, 15 Oct 2026 09:28:30 +0000</pubDate>
      <link>https://dev.to/jean_hoccart_0e38d2242cd2/how-a-server-driven-python-framework-decides-which-part-of-the-page-to-re-render-563l</link>
      <guid>https://dev.to/jean_hoccart_0e38d2242cd2/how-a-server-driven-python-framework-decides-which-part-of-the-page-to-re-render-563l</guid>
      <description>&lt;p&gt;When the server renders your UI, every click raises the same question: &lt;strong&gt;what do you send back?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The usual answers each have a cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-run everything.&lt;/strong&gt; Streamlit's model: the script runs top to bottom on each interaction. Simple to start with, but state lifetimes get blurry as the app grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire it by hand.&lt;/strong&gt; With HTMX you write &lt;code&gt;hx-target&lt;/code&gt; and &lt;code&gt;hx-swap&lt;/code&gt; for each interaction. Precise, but the knowledge of "this button changes that table" lives in attributes scattered through your templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move the state to the browser.&lt;/strong&gt; A React app keeps its own state and its own build pipeline, and the server becomes an API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm building &lt;a href="https://github.com/JeanHoccart/bretzel" rel="noopener noreferrer"&gt;Bretzel&lt;/a&gt;, a full-stack Python framework, around a fourth answer: &lt;strong&gt;regions of the page declare the state they read, and the framework works out the rest.&lt;/strong&gt; This post explains how that works, and where it costs you.&lt;/p&gt;

&lt;h2&gt;
  
  
  A region declares what it reads
&lt;/h2&gt;

&lt;p&gt;Here is a complete page with a counter (it runs on the current release, 0.1.0a2):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bretzel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refreshable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bretzel.state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="nd"&gt;@refreshable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;broadcast&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clicked &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; times&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Counter&lt;/code&gt; is &lt;strong&gt;typed state&lt;/strong&gt; with an explicit lifetime: &lt;code&gt;SessionState&lt;/code&gt; lives as long as the visitor's session. Bretzel also has page, user and app scopes, plus client-only state for things the server never needs to know.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;increment&lt;/code&gt; is a plain function. It doesn't return HTML and doesn't touch the DOM: it &lt;strong&gt;mutates state&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;count&lt;/code&gt; is a &lt;strong&gt;region&lt;/strong&gt;. &lt;code&gt;deps=[Counter]&lt;/code&gt; says "re-render me when &lt;code&gt;Counter&lt;/code&gt; changes".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What happens when you click
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The button sends an action request (HTMX carries it; the action is signed, so a client can't call arbitrary handlers).&lt;/li&gt;
&lt;li&gt;The server runs &lt;code&gt;increment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;At the end of the action, the server &lt;strong&gt;compares the state before and after&lt;/strong&gt;. Every region whose &lt;code&gt;deps=&lt;/code&gt; include a state that changed is re-rendered, and nothing else.&lt;/li&gt;
&lt;li&gt;The response carries only those HTML fragments. The browser merges them into the existing page with idiomorph instead of replacing whole blocks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a handler changes nothing a region depends on, the round trip brings back nothing to swap. You never write "after this action, refresh that table": the dependency is declared once, on the region, next to the code that reads the state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other windows follow: &lt;code&gt;broadcast=&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;deps=&lt;/code&gt; answers "what makes &lt;strong&gt;my&lt;/strong&gt; view re-render". &lt;code&gt;broadcast=&lt;/code&gt; answers "what should &lt;strong&gt;other&lt;/strong&gt; open windows follow".&lt;/p&gt;

&lt;p&gt;When an action changes a state listed in a region's &lt;code&gt;broadcast=&lt;/code&gt;, the server sends a small signal over Server-Sent Events. Each other window then re-fetches that region &lt;strong&gt;in its own context&lt;/strong&gt;. No data travels from one client to another, only the signal "this changed".&lt;/p&gt;

&lt;p&gt;The two lists are orthogonal, and putting a state in both isn't redundant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deps&lt;/code&gt; only: instant for me, nobody else cares (a filter, a draft).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;broadcast&lt;/code&gt; only: I never change it, but others do (a queue fed by workers).&lt;/li&gt;
&lt;li&gt;both: instant for me, pushed to the others (a shared board).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://demo.bretzel-py.dev" rel="noopener noreferrer"&gt;live Kanban demo&lt;/a&gt; shows this best: open it in two windows of the same browser and drag a card in one. The other follows, activity line included, and undo works from either. The server arbitrates every drop: a full column refuses a card simply by not changing the state. The whole app is 9 Python modules and no JavaScript of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;This model isn't free, and I'd rather say so up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every interaction is a round trip.&lt;/strong&gt; On a slow network you feel it. That's why client-only state exists, for things like an open menu or a toggled panel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State lives on the server.&lt;/strong&gt; It's kept in memory by default, with a Redis backend, and each scope has its own lifetime. That's memory you have to think about, which an SPA pushes to the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One gotcha:&lt;/strong&gt; change detection compares states. Reassigning a value (&lt;code&gt;counter.value += 1&lt;/code&gt;, &lt;code&gt;board.cards = [...]&lt;/code&gt;) is always seen. Mutating deep inside nested structures is where you have to be careful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's an alpha&lt;/strong&gt; (0.1.0a2) with one maintainer. APIs will move, and I haven't proven anything about large-scale deployments yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;p&gt;NiceGUI keeps a tree of UI objects on the server for each client. Reflex compiles your app to a Next.js frontend. FastHTML stays deliberately close to HTML and HTMX. I wrote &lt;a href="https://docs.bretzel-py.dev/compare" rel="noopener noreferrer"&gt;a comparison with sources&lt;/a&gt;, including the cases where each of them is the better pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;bretzel
bretzel new hello
&lt;span class="nb"&gt;cd &lt;/span&gt;hello &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
bretzel dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.bretzel-py.dev" rel="noopener noreferrer"&gt;https://docs.bretzel-py.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Components: &lt;a href="https://ui.bretzel-py.dev" rel="noopener noreferrer"&gt;https://ui.bretzel-py.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code (MIT): &lt;a href="https://github.com/JeanHoccart/bretzel" rel="noopener noreferrer"&gt;https://github.com/JeanHoccart/bretzel&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feedback that would help me most: build one small internal tool with it, and tell me exactly where the model, the API or the docs stopped making sense.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>htmx</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
