“hypermedia systems”‘s Contact.app with fasthtml

TL;DR

fasthtml is new, still seems to have some quirks, but is a great tool to use if you want to keep all your frontend logic in python.

Hello again

In my previous blog post I wrote about htmx used with flask and jinja2 as done by the authors of the book “hypermedia systems”.

Having done that one could wonder what that code would look like using fasthtml. This is what this blog post is about :), I know, you are probably shocked beyond belief, if you’ve seen my two previous posts.

Links to things

What is fasthtml?

According to the authors of fasthtml

with FastHTML you can get started on anything from simple dashboards to scalable web applications in minutes.

Bold claims. But the example they give in their docs illustrates the idea pretty well

from fasthtml.common import *
app = FastHTML()

@app.get("/")
def home():
    page = Html(
        Head(Title('Some page')),
        Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass')))
    return page

serve()

So there is some similar routing as with flask, e.g. here with @app.get("/"). But then the great part that allows you to write frontend pieces is in the content of the function. That content is all you need to construct an actual HTML page, from python.

So all you need to learn is how fasthtml works to replace the jinja2 templating and html file business. The examples in their docs and their examples repo also are good starting points.

fasthtml to replace jinja2 templating and flask in the Contact.app

Was I successful swapping out jinja2 and flask with fasthtml? See for yourself :).

My attempt for this can be found in apps/web-fasthtml. It is based on apps/web4 ( so no json data api, but probably something that could be interesting to be done, especially with fastapi :P).

Turns out replacing the html / templating bits with fasthtml is relatively straigtforward, with a few quirks but also some pleasentness.

A good example of what the transition from jinja2 to fasthtml looks like is probably the HTML for the rows of the contacts to be displayed. In the original authors’ HTML that looks like

{% for contact in contacts %}
    <tr>
        <td><input type="checkbox" name="selected_contact_ids"
            value="{{ contact.id }}"></td>
        <td>{{ contact.first }}</td>
        <td>{{ contact.last }}</td>
        <td>{{ contact.phone }}</td>
        <td>{{ contact.email }}</td>
        <td>
            <a href="/contacts/{{ contact.id }}">View</a>
        </td>
        <td>
            <a href="/contacts/{{ contact.id }}/edit">Edit</a>
        </td>
        <td>
            <a
                href="#"
                hx-delete="/contacts/{{ contact.id }}"
                hx-swap="outerHTML swap:1s"
                hx-confirm="Are you sure you want to delete this contact?"
                hx-target="closest tr">Delete</a>
        </td>
    </tr>
{% endfor %}

with fasthtml it looks like

def get_rows(contacts: list[Contact]) -> FT:
    _get_td_view = lambda c: Td(A("View", href=f"/contacts/{c.id}"))
    _get_td_edit = lambda c: Td(A("Edit", href=f"/contacts/{c.id}/edit"))
    _get_td_delete = lambda c: Td(
        A(
            "Delete",
            href="#",
            hx_delete=f"/contacts/{c.id}",
            hx_swap="outerHTML swap:1s",
            hx_confirm="Are you sure you want to delete this contact?",
            hx_target="closest tr",
        )
    )
    rows = Tbody(
        Tr(
            Td(c.first),
            Td(c.last),
            Td(c.phone),
            Td(c.email),
            _get_td_view(c),
            _get_td_edit(c),
            _get_td_delete(c),
        )
        for c in contacts
    )
    return rows

So the order is a bit different, but pretty much 1:1 logic, except no knowledge of how jinja2 templating works is necessary, if you can figure out the proper way of passing arguments with fasthtml :D.

Quirks working with fasthtml

I see a few quirks when working with fasthtml, which may be worth knowing before diving in. Hopefully having read this it will save you some time down the line when you wonder why random 404s appear or data is not passed as expected.

Quirk #1 – Routing

In “hypermedia systems” a DELETE request to /contacts/archive triggers a dedicated function only for that combination of method and path. But in fasthtml this exact same code led to a 404 status code.

I may have overlooked fasthtml docs on how to properly fix this but I resorted to a dedicated path /contacts/archive/delete. That one actually was originally part of the vanilla HTML app but then actively removed in the “hypermedia systems” code in the book as a htmx feature.

This is what the htmx supported HTML looks like

<button hx-delete="/contacts/archive">
    Clear Download
</button>

This is the working fasthtml version

Button(
    "Clear Download", hx_delete="/contacts/archive/delete"
)

So the fasthtml version needs the “/delete” at the end, at least for that specific case. There are other instances where the expected hx_delete works just as expected.

Quirk #2 – Finding tags

Some guesswork is required to find some HTML tags in fasthtml, e.g. I failed to find equivalents for the <all-caps> and <sub-title> tags for the layout.

Quirk #3 – HTML attributes

HTML Tags are represented as functions with similar names as the tags, e.g. H1() can be called to create a <h1> tag.

To set attributes of the tags, one passes arguments to the fasthtml functions. But some arguments are just not visible in the documentation, e.g. “type” or “placeholder” for Input() / <input>.

The arguments for the attributes actually are there, but I only found them by guessing because sometimes those attribute / argument names are slightly different, e.g. for and class. This is because of them being reserved in python already, but then why have more than one way to spell each?

Quirk #4 – Typing

Sort of a minor thing, but it tripped me up a bit. fasthtml is less obsessively typed than other packages, e.g. pydantic ^^. But it does have a clever way of passing data coming from requests to functions, as part as their arguments, if specified.

For example

@app.route("/contacts/{contact_id}/edit", methods=["POST"])
def contacts_edit_post(d: FormData, contact_id: int = 0):
  ...

expects to receive in the body data structures just as FormData and contact_id as an integer from the path.

However those arguments NEED to have the right types, otherwise the hand-over does not work properly. If you forget the types at first you can easily accidentally create bugs.

Pleasentness working with fasthtml

That said. I find the code with fasthtml muuuuuuuch easier to refactor and process mentally than the flask / jinja2 version. Mainly because I only neep to keep in mind htmx principles, and fasthtml handles the rest what is not basic python / HTML.

The handing over of data from front to backend is relatively easy, e.g. in def contacts_new with FormData, if you remember to type it correctly ^^.

So learning fasthtml mostly seems to mean learning HTML and htmx. But you’d have to do that anyways with another approach like with flask and jinja2.

I was also pleasently surprised to find, that at least for the cases I’ve covered in this book that pretty much all HTML pieces required are already covered by fasthtml.

Some sort of final word

I’m very curious to see what sort of adoption htmx and fasthtml will have and how they will be developed. From my vantage point it seems together they do form a very useful combination of tools.

I’ll probably reach for them in the future, over flask / jinja2 or something like streamlit, if I need to build some frontend.

I hope this got you interested as well / saved you some time.

Until next time. Happy coding! 🙂

Writing an interactive dataframe editor with fasthtml

Ever wanted to edit a pandas dataframe object in python in some interactive way felt too limited by streamlit but also did not want to pick up html / css / javascript just for that?

Then one solution could be fasthtml, or python-fasthtml as it is called on PyPI. With that package you can write your client facing web code as well as your server side code from the comfort of python. And, if you manage to wrap your head around it, you can also use htmx. Not quite sure I’m fully there yet myself :-P.

So using fasthtml, I’ve built a small app to edit a dataframe and uploaded it over on GitHub for you: https://github.com/eschmidt42/fasthtml-data-editor

There are two python files, main_simple.py and main_hx.py. The first creates the website in a more vanilla way, reloading the entire page when updating the dataframe. The latter uses htmx and replaces only the dataframe bits.

I’d recommend to compare them side by side. You’ll notice how the differences are quite minor.

References I found useful tinkering with this were

Happy coding!