FastHTML getting started & comparison with Flask

FastHTML getting started & comparison with Flask
Input: "Generate me a learning curve meme graph with the following technologies: It's like traditional HTML, meets stronger typing, meets niche strongly typed web frameworks like ELM / Yesod, meets the simplicity of VanillaJS with the practicality and mass-market adoption appeal of the Python ecosystem without learning curve of Rust."

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.

from fasthtml.common import fast_app, serve, Div, A

app, rt = fast_app(live=False)


@rt("/")
def get():
    return Div(A('Wikipedia', href="https://www.wikipedia.org/"))

serve()
main.py with pip install python-fasthtml See answerdotai/fasthtml Github
*Nit: I discourage the use of import * - especially tutorials where it adds ambiguity and hinders learning. Try not to use import *. It's cited as bad practice in production code also.


Generates the following html out-of-the-box with FastHTML:

<!doctype html></!doctype>

<html>
  <head>
    <title>FastHTML page</title>
    <meta charset="utf-8"></meta>
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"></meta>
    <script src="https://unpkg.com/htmx.org@next/dist/htmx.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@1.3.0/surreal.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css">
    <style>:root { --pico-font-size: 100%; }</style>
  </head>
  <body>
<div>
  <a href="http://google.com">wikipedia</a>
</div>
  </body>
</html>
The minimal FastHTML default html output, plus a rendered Div element with a containing A tag linking to Wikipedia.

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 ⚡

Simplicity of HTML decorated with tags for seamless ajax support using HTMx toggling tasks Done/Not done.

Full code from the above screenshot is at:

GitHub - KarmaComputing/minimal-FastHTML-quickstart
Contribute to KarmaComputing/minimal-FastHTML-quickstart development by creating an account on GitHub.

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:

YouTube
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 do target_id=str(tid) but then your HTML id's would risk not being unique. The prefix todo- results in id="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:

@rt('/toggle/{tid}')
def get(tid:int):  # noqa: F811
    todo = todos[tid]
    todo.done = not todo.done
    todo.update(todo)
    return todo
Note the addition of :int to the def get method

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
Christopher Simpson - Amazon Web Services (AWS) | LinkedIn
Experienced Director with a demonstrated history of working in the information technology… · Experience: Amazon Web Services (AWS) · Education: Northumbria University · Location: Newcastle Upon Tyne · 500+ connections on LinkedIn. View Christopher Simpson’s profile on LinkedIn, a professional commun…

Collect recurring payments with Subscribie - Try Now