Htmy
Async, pure-Python server-side rendering engine for hypermedia applications.
Install / Use
/learn @volfpeter/HtmyREADME
Source code: https://github.com/volfpeter/htmy
Documentation and examples: https://volfpeter.github.io/htmy
htmy
Async, pure-Python server-side rendering engine.
Unleash your creativity with the full power and Python, without the hassle of learning a new templating language or dealing with its limitations!
Key features
- Async-first, to let you make the best use of modern async tools.
- Powerful, React-like context support, so you can avoid prop-drilling.
- Sync and async function components with decorator syntax.
- All baseline HTML tags built-in.
- Async HTML streaming support for optimal TTFB (time to first byte).
- Support for native HTML/XML documents with dynamic formatting and slot rendering, without custom syntax.
- Markdown support with tools for customization.
- Async, JSON based internationalization.
- Built-in, easy to use
ErrorBoundarycomponent for graceful error handling. - Unopinionated: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
- Everything is easily customizable, from the rendering engine to components, formatting and context management.
- Automatic and customizable property-name conversion from snake case to kebab case.
- Compatible with any other templating library through wrappers.
- Fully-typed.
Testimonials
"Thank you for your work on fasthx, as well as htmy! I've never had an easier time developing with another stack." (ref)
"One of the main parts of the FastAPI -> fasthx -> htmy integration I'm falling in love with is its explicitness, and not too much magic happening." (ref)
"Thank you for your work on htmy and fasthx, both have been very pleasant to use, and the APIs are both intuitive and simple. Great work." (ref)
"I love that the language-embedded HTML generation library approach is becoming more popular." (ref)
"Neat approach and it naturally solves the partial templates problem 👍" (ref)
"Great API design!" (ref)
Support
Consider supporting the development and maintenance of the project through sponsoring, or reach out for consulting so you can get the most out of the library.
Installation
The package is available on PyPI and can be installed with:
$ pip install htmy
The package has the following optional dependencies:
lxml(recommended): When installed, it is prioritized overxml.etree.ElementTreeand provides more secure, faster, and more flexible HTML and XML processing. It is used, for example, for Markdown processing. Install with:pip install "htmy[lxml]".
Component libraries
- htmui: BasecoatUI and TailwindCSS components.
Concepts
The entire library -- from the rendering engine itself to the built-in components -- is built around a few simple protocols and a handful of simple utility classes. This means that you can easily customize, extend, or replace basically everything in the library. Yes, even the rendering engine. The remaining parts will keep working as expected.
Also, the library doesn't rely on advanced Python features such as metaclasses or descriptors. There are also no complex base classes and the like. Even a junior engineer could understand, develop, and debug an application that's built with htmy.
Components
Every object with a sync or async htmy(context: Context) -> Component method is an htmy component (technically an HTMYComponentType). Strings are also components, as well as lists or tuples of HTMYComponentType or string objects. In many cases though, you don't even need to create components, simple functions that return components will be sufficient -- you can find out more about this in the Components guide of the documentation.
Using the htmy() method name enables the conversion of any of your pre-existing business objects (from TypedDictss or pydantic models to ORM classes) into components without the fear of name collision or compatibility issues with other tools.
Async support makes it possible to load data or execute async business logic right in your components. This can reduce the amount of boilerplate you need to write in some cases, and also gives you the freedom to split the rendering and non-rendering logic in any way you see fit.
Example:
from dataclasses import dataclass
from htmy import Component, Context, html
@dataclass(frozen=True, kw_only=True, slots=True)
class User:
username: str
name: str
email: str
async def is_admin(self) -> bool:
return False
class UserRow(User):
async def htmy(self, context: Context) -> Component:
role = "admin" if await self.is_admin() else "restricted"
return html.tr(
html.td(self.username),
html.td(self.name),
html.td(html.a(self.email, href=f"mailto:{self.email}")),
html.td(role)
)
@dataclass(frozen=True, kw_only=True, slots=True)
class UserRows:
users: list[User]
def htmy(self, context: Context) -> Component:
# Note that a list is returned here. A list or tuple of `HTMYComponentType | str` objects is also a component.
return [UserRow(username=u.username, name=u.name, email=u.email) for u in self.users]
user_table = html.table(
UserRows(
users=[
User(username="Foo", name="Foo", email="foo@example.com"),
User(username="Bar", name="Bar", email="bar@example.com"),
]
)
)
htmy also provides a powerful @component decorator that can be used on sync or async my_component(props: MyProps, context: Context) -> Component functions and methods to convert them into components (preserving the props typing). You can find out more about this feature in the Function components guide.
Here is the same example as above, but with function components:
from dataclasses import dataclass
from htmy import Component, Context, component, html
@dataclass(frozen=True, kw_only=True, slots=True)
class User:
username: str
name: str
email: str
async def is_admin(self) -> bool:
return False
@component
async def user_row(user: User, context: Context) -> Component:
# The first argument of function components is their "props", the data they need.
# The second argument is the rendering context.
role = "admin" if await user.is_admin() else "restricted"
return html.tr(
html.td(user.username),
html.td(user.name),
html.td(html.a(user.email, href=f"mailto:{user.email}")),
html.td(role)
)
@component
def user_rows(users: list[User], context: Context) -> Component:
# Nothing to await in this component, so it's sync.
# Note that we only pass the "props" to the user_row() component (well, function component wrapper).
# The context will be passed to the wrapper during rendering.
return [user_row(user) for user in users]
user_table = html.table(
user_rows(
[
User(username="Foo", name="Foo", email="foo@example.com"),
User(username="Bar", name="Bar", email="bar@example.com"),
]
)
)
Built-in components
htmy has a rich set of built-in utilities and components for both HTML and other use-cases:
htmlmodule: a complete set of baseline HTML tags.SnippetandSlots: utilities for creating dynamic, customizable document snippets in their native file format (HTML, XML, Markdown, etc.), with slot rendering support.md:MarkdownParserutility andMDcomponent for loading, parsing, converting, and rendering markdown content.i18n: utilities for async, JSON based internationalization.Tag,TagWithProps, andwildcard_tag: base classes and utilities for creating custom XML tags.ErrorBoundary,Fragment,SafeStr,WithContext: utilities for error handling, component wrappers, context providers, and formatting.etree.ETreeConverter: utility that converts XML to a component tree with support for custom HTMY components.
Rendering
htmy.Renderer is the built-in, default renderer of the library.
If you're using the library in an async web framework like FastAPI, then you're already in an async environment, so you can render components as simply as this: await Renderer().render(my_root_component).
If you're trying to run the renderer in a sync environment, like a local script or CLI, then you first need to wrap the renderer in an async task and execute that task with asyncio.run():
import asyncio
from htmy import Renderer, html
async def render_page() -> None:
page = (
html.DOCTYPE.html,
html.html(
html.body(
html.h1("Hello World!"),
html.p("This page was rendered by
