惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
H
Hackread – Cybersecurity News, Data Breaches, AI and More
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
小众软件
小众软件
月光博客
月光博客
D
DataBreaches.Net
L
LangChain Blog
美团技术团队
S
SegmentFault 最新的问题
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
博客园 - 司徒正美
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Help Net Security
阮一峰的网络日志
阮一峰的网络日志
Y
Y Combinator Blog
I
InfoQ
U
Unit 42
Microsoft Azure Blog
Microsoft Azure Blog
J
Java Code Geeks
博客园 - 三生石上(FineUI控件)
腾讯CDC
Martin Fowler
Martin Fowler

Pierce Freeman

A browser for agents | Pierce Freeman The grey market of podcast appearances The way I travel | Pierce Freeman Fixing slow AWS uploads | Pierce Freeman Local tools should still use vaults We solved scratch content first Starting a podcast in 2025 Being late but still being early Automating our home video imports Adding my parents to tailscale A deep dive on agent sandboxes Language servers for AI | Pierce Freeman My simple home podcast studio We need centralized infrastructure | Pierce Freeman Coercing agents to follow conventions using AST validation My unified theory of social selling My personal backup strategy | Pierce Freeman July updates to the homelab How the KV Cache works httpx is the right way to do web requests in Python Reputation is becoming everything | Pierce Freeman Building a (kind of) invisible mac app Updated knowledge in language models Making an ascii animation | Pierce Freeman How speculative decoding works | Pierce Freeman Under the hood of Claude Code Doing things because they're easy, not hard Speeding up sideeffects with JIT in mountaineer Misadventures in Python hot reloading How text diffusion works | Pierce Freeman
Firehot for hot reloading in Python
2025-06-24 · via Pierce Freeman

header

I've spent a lot of time thinking about hot reloading in Python. Firehot is my answer to that problem for web development.

We can make a pretty simple observation about import dependencies: during development, your third-party dependencies (Django, Flask, requests, SQLAlchemy) almost never change. It's only your project code that's constantly being modified. So why restart everything when you could just restart the parts that actually change?

The approach in firehot works by process isolation. If you're careful with how you do it, a fork() in Unix will copy the exact runtime state into a fresh process. A Python interpreter is actually a pretty simple application executable to copy - it's single threaded and uses the GIL for locking C code. We can build up a Python process that preloads all of our third party sys.modules imported anywhere in our project. When our actual code hits that import it will be a no-op because it has already imported the bytecode.

The result is that when you change your project code, only your project modules get reloaded. The expensive third-party imports (Django, NumPy, etc) stay loaded and cached. Startup time can drop from 5+ seconds to 200ms.

Here's what a typical reload cycle looks like:

from firehot import isolate_imports
from os import getpid, getppid

def run_under_environment(value: int):
   parent_pid = getppid()
   print(f"Running with value: {value} (pid: {getpid()}, parent_pid: {parent_pid})")

# Scan through the current package and determine all imports
# Load these into a template environment that we can fork()
with isolate_imports("my_package") as environment:
   # Each exec will be run in a new process with the environment isolated, inheriting
   # the package's third party imports without having to re-import them from scratch.
   context1 = environment.exec(run_under_environment, 1)

   # These can potentially be long running - to wait on the completion status, you can do:
   result1 = environment.communicate_isolated(context1)

   # If you change the underlying my_project files on disk to add an import, you can run update_environment.
   environment.update_environment()

   # Subsequent execs will use the updated environment.
   context2 = environment.exec(run_under_environment, 2)
   result2 = environment.communicate_isolated(context2)

My mental model for this pipeline is like Dockerfiles: you build up each individual layer with package dependencies. If you need to reload any of them, you just need to unroll layers1 until you reach the one where they were declared and rebuild it.

The Import Tax

Python's import system has always been designed for production environments where you start once and run forever. But this creates an import tax that your dev workflow has to bear. You pay the fixed cost you pay every time your application boots. The tax gets higher as your dependency tree grows.

Modern Python applications routinely import hundreds of packages at startup. Each package can trigger its own initialization logic: database drivers establishing connection pools, ML libraries loading models into memory, web frameworks scanning for route definitions. What started as simple import requests statements compound into multi-second delays.

The conventional wisdom has been to live with it. "Just use gunicorn in production and accept the slow development cycle." But this feels backward. We've optimized for the 0.1% of time when code boots up in production at the expense of the 99.9% of bootup time when developers are iterating.

Firehot flips this equation. Instead of paying the import tax on every restart, you pay it once per session. The template process becomes your cached dependency state, and forking becomes your reload mechanism.

Why Fork Works

Python's import system is largely read-only after initialization. Once sys.modules is populated, imports become dictionary lookups. This is annoying if you're trying to manipulate the runtime imports but works pretty nicely in a forking context. The mutability of imports and isolation of forked processes let clients inherit the full import state without corrupting the parent.

If your updated code introduces a new third-party import, Firehot detects this via AST parsing and knows to rebuild the template process. If you just changed business logic, it skips the expensive rebuild and forks immediately.

This creates a performance profile that matches your development workflow: fast iterations for code changes, slower rebuilds only when you actually change dependencies. Most development sessions involve hundreds of code tweaks for every new package added.

The Rust Bridge

Writing this in pure Python would be possible but painfully slow. Python's subprocess management and AST parsing are decent, but not optimized for the tight loops that hot reloading demands. Every millisecond matters when you're trying to stay in flow state.

Firehot is mostly written in Rust, especially for the performance-critical paths: file watching, process management, and dependency analysis. Python handles the higher-level orchestration and API surface. It's the same pattern that's made tools like ruff and uv so much faster than their pure-Python predecessors. AST parsing is blazing fast in the Rust ecosystem.

The architecture also means Firehot can work with any Python web framework or application structure. It doesn't need to understand Django's auto-reloader or Flask's debug mode - it operates at the import level, which is universal across Python applications.

In practice, this feels like having your cake and eating it too. You get the ergonomics of rapid development cycles without sacrificing the rich ecosystem that makes Python productive in the first place. The expensive imports happen once, then fade into the background where they belong.

  1. Processes for Firehot, filesystem layers for docker. ↩