I’ve been experimenting with FastHTML for making quick demo apps, often involving language models. It’s a pretty simple but powerful framework, which allows me to deploy a client and server in a single main.py – something I appreciate a lot for little projects I want to ship quickly. I currently use it how you might use streamlit.

I ran into an issue where I was struggling to submit a form with multiple images.

I started with an app that could receive a single image upload from this example.

These examples assume the code is in a main.py file and run with

uvicorn main:app --reload
from fasthtml.common import *

app, rt = fast_app()


@rt("/")
def get():
    inp = Input(type="file", name="image", multiple=False, required=True)
    add = Form(
        Group(inp, Button("Upload")),
        hx_post="/upload",
        hx_target="#image-list",
        hx_swap="afterbegin",
        enctype="multipart/form-data",
    )
    image_list = Div(id="image-list")
    return Title("Image Upload Demo"), Main(
        H1("Image Upload"), add, image_list, cls="container"
    )


@rt("/upload")
async def upload_image(image: UploadFile):
    contents = await image.read()
    print(contents)
    filename = image.filename
    return filename

The contents of the images prints in the console and the filename shows up in the browser.

To support multiple files/images, I tried the following:

from fasthtml.common import *
import uvicorn
import os

app, rt = fast_app()


@rt("/")
def get():
    inp = Input(type="file", name="images", multiple=True, required=True)
    add = Form(
        Group(inp, Button("Upload")),
        hx_post="/upload",
        hx_target="#image-list",
        hx_swap="afterbegin",
        enctype="multipart/form-data",
    )
    image_list = Div(id="image-list")
    return Title("Image Upload Demo"), Main(
        H1("Image Upload"), add, image_list, cls="container"
    )


@rt("/upload")
async def upload_image(images: List[UploadFile]):
    print(images)
    filenames = []
    for image in images:
        contents = await image.read()
        filename = image.filename
        filenames.append(filename)
    return filenames

When we pick and upload multiple files, this code breaks, but with the print statement we can see the data we uploaded.

[UploadFile(filename=None, size=None, headers=Headers({})), UploadFile(filename=None, size=None, headers=Headers({}))]

Not quite what I expected.

After a bit of searching, I learned that a fasthtml function signature can be any compatible starlette function signature (source). With this knowledge, I tried the following approach:

from fasthtml.common import *

app, rt = fast_app()


@rt("/")
def get():
    inp = Input(
        type="file", name="images", multiple=True, required=True, accept="image/*"
    )
    add = Form(
        Group(inp, Button("Upload")),
        hx_post="/upload",
        hx_target="#image-list",
        hx_swap="afterbegin",
        enctype="multipart/form-data",
    )
    image_list = Div(id="image-list")
    return Title("Image Upload Demo"), Main(
        H1("Image Upload"), add, image_list, cls="container"
    )


@rt("/upload")
async def upload_image(request: Request):
    form = await request.form()
    images = form.getlist("images")
    print(images)
    filenames = []
    for image in images:
        contents = await image.read()
        filenames.append(image.filename)
    return filenames

This approach successfully rendered the titles of two images when I uploaded them in a single request as expected.