Skip to content

Repository files navigation

Hyper

CI PyPI License: MIT

Write type-safe HTML in .hyper files. Import components directly from Python.

uv add hyperhtml

Requires Python 3.10 or newer.

Create a component

Create app/pages/Greeting.hyper:

<h1>Hello, World!</h1>

Import and call it:

from app.pages import Greeting

print(Greeting())
<h1>Hello, World!</h1>

The filename becomes the component name. Hyper compiles it on first import.

Pass data

Add typed props above ---:

name: str
---
<h1>Hello, {name}!</h1>
print(Greeting(name="Ada"))
<h1>Hello, Ada!</h1>

Props are keyword-only. Values are escaped by default.

Add a nested component

Use component for reusable markup inside the file:

# app/pages/Dashboard.hyper
title: str
---
component Header(*, title: str):
    <header><h1>{title}</h1></header>
end

<{Header} title={title} />

The file exports Dashboard and its nested Header:

from app.pages import Dashboard

Dashboard(title="Account")
Dashboard.Header(title="Account")

Both calls return:

<header><h1>Account</h1></header>

Dashboard("Account") raises TypeError because props are keyword-only.

Pass content

Place {...} where caller content belongs:

# app/components/Card.hyper
title: str
---
<article>
    <h2>{title}</h2>
    <main>{...}</main>
</article>

Pass content between component tags:

# app/pages/Confirm.hyper
from app.components import Card
---
<{Card} title="Delete item">
    <p>This cannot be undone.</p>
</{Card}>
<article><h2>Delete item</h2><main><p>This cannot be undone.</p></main></article>

Group components in one file

A file with declarations and no rendered output is a component library:

# app/components/forms.hyper
component Button(*, label: str, **attrs):
    <button {**attrs}>{label}</button>
end

component Input(*, name: str, type: str = "text"):
    <input {name} {type} />
end

Import its components like a Python module:

from app.components.forms import Button, Input

Button(label="Save", disabled=True)
Input(name="email", type="email")
<button disabled>Save</button>
<input name="email" type="email">

Use Python

Python statements and expressions stay Python:

items: list[str]
show_count: bool = True
---
<ul>
    for item in items:
        <li>{item.upper()}</li>
    end
</ul>

if show_count:
    <p>{len(items)} items</p>
end

Imports, functions, classes, match, try, with, and async code use the same block form.

Split long tags

Formatting whitespace between attributes does not render:

<{Button}
    label="Save"
    class={["button", {"active": active}]}
    hx-post="/save"
/>

Stream output

Calling a component joins its chunks. .stream() returns the generated iterator:

html = Feed(posts=posts)
chunks = Feed.stream(posts=posts)
from fastapi.responses import StreamingResponse


@app.get("/feed")
def feed():
    return StreamingResponse(
        Feed.stream(posts=posts),
        media_type="text/html",
    )

Use Django or Jinja

Django views can return a component directly:

from django.http import HttpResponse


def home(request):
    return HttpResponse(Dashboard(title="Account"))

Jinja can discover .hyper files from its template paths:

env.add_extension("hyperhtml.integrations.jinja2.HyperExtension")
{{ Dashboard(title="Account") }}

See Integrations for Django templates, Jinja slots, Flask, Litestar, and Sanic.

How files import

.hyper file Python import
Contains rendered output or --- Component named after the file
Contains declarations only Normal module with exported names
Has an adjacent .py file Python file wins

Implicit file components must live inside a package. Root-level .hyper files can be component libraries.

IDE support

  • JetBrains: syntax highlighting, Python language injection, and compiler diagnostics
  • TextMate and VS Code: syntax highlighting

Documentation

License

MIT

About

Type-safe templates for Python, powered by Rust.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages