Skip to content

HTML in Python f-strings

htmf is a collection of utilities for writing HTML in Python f-strings. It works great for typed UI components.

Also it is the suite of tools to boost the developer experience:

Installation

pip install htmf

Working example

from dataclasses import dataclass
from flask import Flask, url_for, request

import htmf as ht
from htmf import Safe


@dataclass
class Soup:
    name: str
    countries: list[str]
    is_chunky: bool


def SoupCard(id: int, soup: Soup) -> Safe:
    return ht.m(
        f"""
        <div class="card mb-2">
            <div class="card-body">
                <h5 class="{ ht.c("card-title", soup.is_chunky and 'text-danger') }" >
                    { ht.t(soup.name) }
                </h5>
                <p class="card-text">
                    Countries:
                    { ht.t(', '.join(soup.countries) if soup.countries else '?') }
                </p>
                <a href="{ ht.t(url_for('soup', id=id)) }" class="card-link">Go</a>
            </div>
        </div>
        """
    )


def SoupsCollection(soups: dict[int, Soup], limit: int) -> Safe:
    selected = list(soups.items())[:limit]

    return ht.m(
        f"""
        <div class="container">
            <h4>Showing first { ht.t(limit) } soups:</h4>
            { ht.t(SoupCard(*soup) for soup in selected) }
        </div>
        """
    )


def Layout(body: Safe, title: str):
    bootstrap = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
    return ht.document(
        f"""
        <!doctype html>
        <html>
            <head>
                <title>{ ht.t(title) }</title>
                <link href="{ bootstrap }" rel="stylesheet">
            </head>
            <body>
                { body }
            </body>
        </html>
        """
    )


app = Flask(__name__)

# https://en.wikipedia.org/wiki/List_of_soups
SOUPS = {
    1: Soup("Aguadito", ["Peru"], True),
    2: Soup("Ant egg soup", ["Laos", "Northeastern Thailand"], True),
    3: Soup("Beef noodle soup", ["East Asia"], False),
    4: Soup("Chicken soup", [], False),
    5: Soup("Tom Yum", ["Thailand"], True),
}


@app.route("/")
def index():
    limit = request.args.get("limit", type=int, default=len(SOUPS))
    return Layout(SoupsCollection(SOUPS, limit=limit), title="Soups list")


@app.route("/<int:id>")
def soup(id: int):
    found = SOUPS.get(id)
    if not found:
        return Layout(ht.t("Unknown soup id"), title="Soup 404")
    return Layout(SoupCard(id, found), title=found.name)

You can run the tag-soup-inspired example with flask --app=soup.py run

Few things to note:

  • It's just the normal Python code without any magic
  • Syntax highlighting makes the HTML look like, well, HTML
  • UI is composed from components
  • Components functions names are not PEP-8. It's intentional to make it look more JSX-y.

The main utilities are:

  • ht.m - wraps the markup and triggers syntax highlighing, linting and formatting.
  • ht.t - composes text and components, HTML-escapes and concatenates them
  • ht.c - composes classes in a way resembling the popular classnames library.
  • Safe - noop subclass of str. Used in runtime to tell HTML-escaped from non-escaped strings. Also used in linting to prove the f-expressions are safe.