<?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: Muspi Merol</title>
    <description>The latest articles on DEV Community by Muspi Merol (@muspi-merol).</description>
    <link>https://dev.to/muspi-merol</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F992807%2Fe4d0737f-3a69-47f1-933d-40934309ec76.png</url>
      <title>DEV Community: Muspi Merol</title>
      <link>https://dev.to/muspi-merol</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muspi-merol"/>
    <language>en</language>
    <item>
      <title>Why Not Implement HMR with Static Analysis?</title>
      <dc:creator>Muspi Merol</dc:creator>
      <pubDate>Sat, 12 Jul 2025 22:28:23 +0000</pubDate>
      <link>https://dev.to/muspi-merol/why-not-implement-hmr-with-static-analysis-2l5d</link>
      <guid>https://dev.to/muspi-merol/why-not-implement-hmr-with-static-analysis-2l5d</guid>
      <description>&lt;p&gt;A few months ago, I came across an article called &lt;a href="https://www.gauge.sh/blog/how-to-build-hot-module-replacement-in-python" rel="noopener noreferrer"&gt;How to build Hot Module Replacement in Python&lt;/a&gt;, which talked about using their Python static analysis tool &lt;a href="https://github.com/gauge-sh/tach" rel="noopener noreferrer"&gt;Tach&lt;/a&gt; to generate a dependency graph in a key-value format like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "a.py": ["b.py", "c.py"],
  "b.py": ["d.py"],
  "d.py": ["e.py"],
  "c.py": ["f.py"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the way, this project became unmaintained last month 😅. I only found out after &lt;a href="https://github.com/gauge-sh/tach/issues/791" rel="noopener noreferrer"&gt;asking&lt;/a&gt; that the developer left to start an AI company. I think someone in the comments put it well:&lt;br&gt;&lt;br&gt;
I guess that's the issue with open source tools being backed by VCs. These tools will never be maintained if you can't make tons of money off of them.&lt;/p&gt;

&lt;p&gt;The basic idea is that whenever a file changes, you use the dependency graph to update (i.e., re-import) that file and all the modules it directly or indirectly affects.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Python developers usually don’t like splitting files too finely, so in practice, using this approach often means that a lot of files get reloaded each time (sometimes, changing any file could trigger a reload of every module in the project). Compared to a regular cold reload, this solution only saves the time spent importing third-party libraries and the overhead of starting Python itself. But you still lose all intermediate state.&lt;/p&gt;

&lt;p&gt;On top of that, lazy loading and dynamic imports can’t be captured by static analysis.&lt;/p&gt;
&lt;h3&gt;
  
  
  Our Runtime Solution
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/hmr/" rel="noopener noreferrer"&gt;&lt;code&gt;hmr&lt;/code&gt;&lt;/a&gt; takes a completely different approach by recording dependencies at runtime. Unlike &lt;code&gt;tach&lt;/code&gt;, which uses static analysis of the AST to determine dependencies, &lt;code&gt;hmr&lt;/code&gt; tracks which modules are imported and which variables from those modules are actually used, building the dependency graph that way.&lt;/p&gt;

&lt;p&gt;For example, suppose you have a file &lt;code&gt;data.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a = 1
b = 2
c = a + b  # 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And another file &lt;code&gt;a.py&lt;/code&gt; uses a value from &lt;code&gt;data.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from data import a
print(a)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With static analysis, &lt;code&gt;a.py&lt;/code&gt; would be considered dependent on &lt;code&gt;data.py&lt;/code&gt;, so if &lt;code&gt;data.py&lt;/code&gt; is reloaded, &lt;code&gt;a.py&lt;/code&gt; would be reloaded too.&lt;/p&gt;

&lt;p&gt;But with &lt;code&gt;hmr&lt;/code&gt;, we support much finer-grained reactivity. That means &lt;code&gt;a.py&lt;/code&gt; is only considered dependent on the value of &lt;code&gt;a&lt;/code&gt; in &lt;code&gt;data.py&lt;/code&gt;. So if you change &lt;code&gt;b = 2&lt;/code&gt; to &lt;code&gt;b = 3&lt;/code&gt; in &lt;code&gt;data.py&lt;/code&gt;, the value of &lt;code&gt;data.a&lt;/code&gt; hasn’t changed, so &lt;code&gt;a.py&lt;/code&gt; won’t reload.&lt;/p&gt;

&lt;p&gt;Only if you change &lt;code&gt;a = 1&lt;/code&gt; to something else will &lt;code&gt;a.py&lt;/code&gt; be triggered to reprint the value of &lt;code&gt;a&lt;/code&gt;. Similarly, if your entry file is &lt;code&gt;c.py&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import data
print(data.c)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then changing either &lt;code&gt;a&lt;/code&gt; or &lt;code&gt;b&lt;/code&gt; (as long as the result of &lt;code&gt;a + b&lt;/code&gt; changes) will cause &lt;code&gt;c&lt;/code&gt; to change, and &lt;code&gt;data.c&lt;/code&gt; will be reprinted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4torefregdu5hlpopa8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4torefregdu5hlpopa8.gif" alt=" " width="800" height="715"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the kind of fine-grained hot reloading that static analysis can never achieve.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;It’s actually pretty simple. Basically, I just need to record every time a module’s &lt;code&gt;__getattr__&lt;/code&gt; is accessed. For example, if module &lt;code&gt;a&lt;/code&gt; triggers &lt;code&gt;b.__getattr__("c")&lt;/code&gt;, that means somewhere in &lt;code&gt;a.py&lt;/code&gt; there’s a &lt;code&gt;from b import c&lt;/code&gt; or &lt;code&gt;b.c&lt;/code&gt;, so module &lt;code&gt;a&lt;/code&gt; depends on &lt;code&gt;b.c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As for how to customize a module’s &lt;code&gt;__getattr__&lt;/code&gt;, it’s just regular metaprogramming (I explained this in detail in &lt;a href="https://muspimerol.site/blog/yfw3tf2h1mzghaz7" rel="noopener noreferrer"&gt;this article&lt;/a&gt;). In short, you implement a custom subclass of ModuleType and register a ModuleFinder in &lt;code&gt;sys.meta_path&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;After more than half a year of work, I’ve handled tons of edge cases and solved many performance issues, all to make sure the magic works transparently and users never have to worry about the dragons under the hood.&lt;/p&gt;

&lt;p&gt;Now, &lt;code&gt;hmr&lt;/code&gt; as a CLI fully mimics Python’s behavior. For example, if you usually start your app with &lt;code&gt;python main.py -a --b c&lt;/code&gt;, you can just switch to &lt;code&gt;hmr main.py -a --b c&lt;/code&gt; and everything should just work.&lt;/p&gt;

&lt;p&gt;If you use Uvicorn to start a web service, you can also try &lt;a href="https://pypi.org/project/uvicorn-hmr/" rel="noopener noreferrer"&gt;&lt;code&gt;uvicorn-hmr&lt;/code&gt;&lt;/a&gt;, which is a drop-in replacement for &lt;code&gt;uvicorn --reload&lt;/code&gt;—just swap the command and you’re good to go. I’ve been very careful about backward compatibility.&lt;/p&gt;

&lt;p&gt;The package names match the CLI names: for example, &lt;code&gt;hmr&lt;/code&gt; is installed with &lt;code&gt;pip install hmr&lt;/code&gt;, and &lt;code&gt;uvicorn-hmr&lt;/code&gt; with &lt;code&gt;pip install uvicorn-hmr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Recently, I also made a fun and even more hacky tool called &lt;a href="https://pypi.org/project/hmr-daemon/" rel="noopener noreferrer"&gt;&lt;code&gt;hmr-daemon&lt;/code&gt;&lt;/a&gt;. After &lt;code&gt;pip install hmr-daemon&lt;/code&gt;, you don’t need to change any code, but whenever you modify a file and the Python process hasn’t exited yet, that module will be reloaded, along with any affected modules in the correct order. This is especially useful when you’re using your project’s modules in a REPL like &lt;code&gt;ipython&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>dx</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
