FastHTML getting started & comparison with Flask
FastHTML is built on-top of the popular python libraries starlette, and uvicorn. FastHTML introduces Python types for common HTML elements integrated with HTMX which means you don't need to write html templates directly, and save a lot of time (in theory).
It's like traditional HTML, meets stronger typing, meets niche strongly typed web frameworks (ELM / Yesod), meets the simplicity of VanillaJS with the practicality and mass-market adoption appeal of the Python ecosystem without learning curve of Rust. If I could borrow some of your time, here's my notes and musings on this exciting FastHTML thing.
A quick example FastHTML snippet below to illustrate the Python types for common HTML elements the framework defines. I've intentionally been explicit* with the python imports to make it clear to the reader/learner what is provided by FastHTML.
*Nit: I discourage the use ofimport *
- especially tutorials where it adds ambiguity and hinders learning. Try not to useimport *
. It's cited as bad practice in production code also.
Generates the following html out-of-the-box with FastHTML:
Consequently, when working with FastHTML (or JSx or Jinja for that matter) find yourself living in view-source:http://127.0.0.1:5001/?
to see the rendered HTML to visualise/debug it as you're working.
Notice that the default FastHTML page render always sends to the browser the following:
- A boilerplate HTML structure including
<!doctype html>
* - htmx Javascript from the htmx project
- surreal.js which is a "Mini jQuery alternative" - forked by AnswerDotAI of gnat/surreal project, but appears maintained by same author.
- css-scope-inline which is "Scope your inline style tags in pure vanilla CSS" gnat/css-scope-inline
- Picocss - "Minimal CSS Framework"
For when it matters, in total it's only about 40.7Kb, (see How web bloat impacts users with slow connections). If you're curious about on-device footprints being small (such as in low-powered electronics needing web endpoints), see Miguel Grinberg's Microdot project which is "Small enough to work with MicroPython, while also being compatible with CPython".
*(psst.. line 1 this is the first time I've seen a closing tag for </!doctype>
, is that even valid HTML? Answer: Apparently not, still not 100% sure. Issue raised, and fixed🚀- thanks Jeremy!)
HTMx Instant speed satisfaction ⚡
Full code from the above screenshot is at:
When wanting to perform traditional GET
/ POST
/ PUT
(etc) HTTP methods, FastHTML uses HTMx for its abstractions over Ajax
/ fetch
. You get a very quick dopamine hit the first time you experience the coming-together of HTMx and HTML. The reduction of moving parts, without compromising benefits.
“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”
― Antoine de Saint-Exupéry, Airman's Odyssey
I'd like to stress these ajax-y/fetch benefits are coming from the HTMx project which stands in its own right. HTMx can be used in any web project including other languages/frameworks - without needing FastHTML. I encourage you to check it out, especially their twitter: @htmx_org. What's elegant about FastHTML is the HTML elements it defines in Python allow you to specify the HTMx tags (such as hx-post
) right in Python, and FastHTML generates the correctly decorated HTML for you.
Note: HTMx has recently (2024) released HTMx version 2. Thankfully FastHTML already uses HTMx version 2.
Getting started with FastHTML 🎥 tutorial notes
As I followed the FastHTML tutorial video, I took notes on errors I encountered and document the various error you may also see with helpers to resolve them. These can be helpful when thown unfamiliar errors within a new codebase.
Error's you may see following 'Getting started with FastHTML' video by Jeremy Howard:
Not knowing that url arguments must be typed in FastHTML
if target_id: kwargs['hx_target'] = '#'+target_id
~~~^~~~~~~~~~
TypeError: can only concatenate str (not "int") to str
The above is a HTMx error telling you you've past an Integer
for target_id
rather than a string:
Before fix:
def render(todo):
tid = todo.id
toggle = A('Toggle', hx_get=f'/toggle/{todo.id}', target_id=tid)
return Li(toggle, todo.title, id=tid)
Code after fix:
def render(todo):
tid = f'todo-{todo.id}'
toggle = A('Toggle', hx_get=f'/toggle/{todo.id}', target_id=tid)
return Li(toggle, todo.title, id=tid)
You could also simply dotarget_id=str(tid)
but then your HTML id's would risk not being unique. The prefixtodo-
results inid="todo-1
",id="todo-2
" etc. If you had other elements on a page (e.g. a list of people) you'd want to avoid using only the number.
You see "fastlite/kw.py NotFoundError"
If you're used to Flask, you'll know that route arguments don't require type signatures. Though not required in Starlette framework's routing system (which FastHTML is built upon), they appear required in FastHTML. If you're curious here's a small example using Starlette's routing system without using FastHTML on-top of Starlette.
In the FastHTML tutorial, the root cause of the fastlite/kw.py NotFoundError
mistake* is helpfully made on the "`/toggle/id`" tid endpoint, which is is None
when clicking a TODO list item.
> It's helpful in the sense these mistakes are kept in tutorials for learning- I would't be surprised if Jeremy makes these on purpose- he's an outstanding and well-known teacher).
Example invalid route:
@rt('/toggle/{tid}')
def get(tid): # noqa: F811
todo = todos[tid]
todo.done = not todo.done
todo.update(todo)
return todo
Example corrected route with :int
added for type information:
You see AttributeError: 'Items' object has no attribute 'update'
If like me you manually type things out when learning from a tutorial, you may trip up on the following mistake (wrong):
@rt('/toggle/{tid}')
def get(tid:int): # noqa: F811
todo = todos[tid]
todo.done = not todo.done
todo.update(todo)
return todo
vs below (corrected)
@rt('/toggle/{tid}')
def get(tid:int): # noqa: F811
todo = todos[tid]
todo.done = not todo.done
todos.update(todo)
return todo
This is a chance to inspect the database helpers FastHTML has bundled out of the box. Let's dig into the type of todos
by putting a breakpoint()
after todo.done
and inspect todos
type:
todos.__class__
<class 'sqlite_minutils.db.Table'>
Above we can see FastHTML is using the sqlite-minutils
python package which appears to provide a simple helpful wrapper around common SQLite operations within Python (syntactic sugar). Makes me nervous when I see such projects being so new (2 months at the time of writing, but less nervous when considering the people behind such projects, however it's a hard(?) fork of sqlite-utils
which has a long (over 4 years) history and venerable provenance from Simon Willison (co-creator of Django). Doing things like this can create a sunk cost maintenance burden in the future needing to back-port improvements / features which may come from either side.
Final thoughts after quick-start with FastHTML
What a breeze, what a lovely coming together of web technologies!
I'm left with the thirst to get good patterns nailed/documented for:
- Database migrations (I'm used to using Flask-Migrate by Miguel Grinberg which is a wrapper for SQLAlchemy's Alembic- a fantastic database schema migration)
- Integration with SQLAlchemy generally. I'm still learning to love FastHTML's (very?) opinionated way to model databases- new commers will get used to tearing down their database and aren't (yet) presented with patterns to handel schema migration. Documented patterns for schema migrations is one thing I'm looking for to make day two operations (production) a thing. It's early days (and open source welcoming contributions..)
- A clearer understanding of how to handle multiple databases. In the tutorial it's not clear to be how multiple databases are modeled (esecially when multiple collumns get added as arguments to
fast_app
. The tutorial, by design is simple and may be a case of RTFM) - Does FastHTML have an equivalent to Flask's
url_for
? Yes. The docs don't seem to reference that. Starlette does-,Reverse%20URL%20lookups,-You%27ll%20often%20want) so it's a matter of documentation. I was concerned about when paths change and having to update all those strings (this goes against the leaning toward types FastHTML has with it's elements) - and now just getting carried away / hungry for docs on
- Integrating translations (i18n/gettext)
- JSON endpoints examples - I'm loving
htmx
as much as the next person, but what are the patterns for this? The htmx author has some references for exactly this already