Hyperclass
Subclass the web.
Hyperclass is a small experiment in building interactive web applications as Python class hierarchies.
HTML elements are Python base classes. Python subclasses become CSS classes. Styles follow inheritance. Routes are methods. Decorated handlers are URLs. Interaction is ordinary HTTP over WSGI, with htmx 4 in the browser.
pip install hyperclass
Sixty-second tour
from dataclasses import dataclass
from hyperclass import (
App, button, css, div, form, get, grid, hx, input, outer_morph,
name, post, rem,
)
class card(div):
style = css(
display=grid,
gap=1 * rem,
padding=1.25 * rem,
border="1px solid #ddd",
border_radius=.75 * rem,
)
class guest_name(input):
name = name.name
placeholder = "Your name"
required = True
class guest_form(form):
style = css(display=grid, gap=.75 * rem)
def content(self):
yield guest_name()
yield button("Say hello", type="submit")
@dataclass
class Guest:
name: str
class guestbook(App):
@get("/")
def index(self, request):
return card(
"Who are you?",
guest_form(
hx=hx.post(
guestbook.create,
target=card,
swap=outer_morph,
)
),
)
@post("/guests")
def create(self, request, form: Guest):
return card(f"Hello, {form.name}!")
app = guestbook(title="Guestbook")
Run it:
python -m hyperclass myapp:app
Then open http://127.0.0.1:8000. There is no JavaScript build, template language, ASGI dependency, or CSS file hidden elsewhere.
HTML classes are Python classes
Every built-in element can be subclassed:
from hyperclass import css, div, grid, orange, rem
class card(div):
style = css(display=grid, gap=1 * rem, padding=1.25 * rem)
class warning_card(card):
style = css(
border_color=orange,
background=orange.fade(0.08),
)
Calling:
warning_card("Something happened")
produces ordinary, inspectable HTML:
<div class="card warning-card">Something happened</div>
The first built-in HTML ancestor determines the tag. Each semantic subclass
contributes a CSS class. snake_case becomes kebab-case.
Multiple inheritance composes behavior and styles:
class compact:
style = css(padding=.5 * rem)
class clickable:
style = css(cursor="pointer")
class result_card(card, compact, clickable):
pass
<div class="card compact clickable result-card"></div>
Components use normal Python state and methods:
from hyperclass import strong
class greeting(card):
def __init__(self, name):
self.name = name
def content(self):
yield "Hello, "
yield strong(self.name)
Text and attribute values are escaped by default. markup(...) is the explicit
escape hatch for trusted HTML.
HTML attributes inherit too
Non-private class values become default HTML attributes:
from hyperclass import a, input, name
class external_link(a):
target = "_blank"
rel = "noreferrer"
class url_field(input):
type = "url"
name = name.url
required = True
autocomplete = "url"
The defaults follow the same base-to-derived order as styles. Subclasses and multiple-inheritance mixins can override them. Attributes passed to an instance win last:
external_link("Same tab", href="/", target="_self", rel=None)
None and False suppress an inherited attribute. Underscores in Python names
become hyphens, so aria_label renders as aria-label. Boolean True renders
as a valueless HTML attribute. An hx = hx.get(...) class default expands into
the corresponding htmx attributes.
CSS is Python too
Base styles, pseudo-states, and media rules live on the component:
from hyperclass import button, css, media, rem
class primary_button(button):
style = css(
padding=".7rem 1rem",
background="#6d28d9",
color="white",
border=0,
border_radius=.5 * rem,
)
hover = css(background="#5b21b6")
focus_visible = css(outline="3px solid #c4b5fd")
narrow = media(max_width=40 * rem, width="100%")
Pages collect only the rules used by their element tree. Rules use the concrete semantic class chain as their selector, so Python inheritance and the CSS cascade cooperate even when new component styles arrive later.
Classes, IDs, and names are selectors
Classes can be used directly anywhere a selector is expected:
hx.get(search, target=result_card)
closest(card)
IDs are lazy, interned Python objects:
from hyperclass import id, span
span("3 unread", id=id.unread_count)
hx.get(count, target=id.unread_count)
assert id.unread_count is id.unread_count
As an HTML attribute, id.unread_count renders as unread-count. As a
selector, it renders as #unread-count.
Form names work the same way while preserving Python underscores:
from hyperclass import name
input(name=name.search_query)
request.form[name.search_query]
hx.get(search, include=name.search_query, target=id.results)
As an attribute, name.search_query renders as search_query. As a selector,
it renders as [name="search_query"]. Repeated access returns the same object.
Routes are references, not strings
Application subclasses collect decorated method routes:
from hyperclass import App, get, patch
class bookmarks(App):
@get("/")
def index(self, request):
return bookmark_list(...)
@patch("/bookmarks/<int:bookmark_id>")
def toggle(self, request, bookmark_id):
return bookmark_card(...)
Decorated handlers retain their route metadata:
bookmarks.toggle.url(bookmark_id=42)
# '/bookmarks/42'
hx.patch(
bookmarks.toggle,
bookmark_id=42,
target=bookmark_card,
)
Typed path parameters are converted before the handler runs. Query strings can
be attached with .url(query={...}) or the query= option on an htmx request.
Forms bind to dataclasses
Annotate a route parameter with a dataclass and Hyperclass builds it from the submitted form:
@dataclass
class NewBookmark:
url: str
title: str = ""
class bookmarks(App):
@post("/bookmarks")
def create(self, request, form: NewBookmark):
self.store.add(form.url, form.title)
return bookmark_list(...)
Binding supports strings, integers, floats, booleans, optional values, and
lists or tuples of those values. Dataclass defaults remain defaults. Invalid or
missing required values produce a 400 Bad Request; application validation can
return a more specific Response.
The underlying values remain available as request.form, request.query,
.get(...), .getlist(...), and .int(...) when explicit parsing is clearer.
Those accessors accept first-class name.* objects as well as strings.
WSGI and htmx 4
A Hyperclass application is a normal WSGI callable. Use the standard-library development server:
python -m hyperclass package.module:app
python -m hyperclass package.module:app --host 0.0.0.0 --port 9000
Production can use any WSGI server. Returning an element from an ordinary browser request wraps it in a complete page. Returning the same element to an htmx request sends only the fragment to swap.
Page(...) controls the document explicitly. Pages include a pinned htmx 4
asset from jsDelivr. htmx 4 <hx-partial> responses can update several
object-selected regions from one request.
When an htmx response introduces a component that was not present on the first
page, Hyperclass includes its CSS in a partial targeting the page's stable
hyperclass-styles stylesheet. The new fragment is styled immediately, without
a reload or a global CSS build.
Try the examples
Clone the repository and run the persistent SQLite bookmark inbox:
git clone https://github.com/grantjenks/python-hyperclass
cd python-hyperclass
python -m hyperclass examples.bookmarks:app
The bookmark app adds, searches, filters, edits, marks, and deletes bookmarks. Its implementation is Python plus SQLite, WSGI, generated CSS, and htmx. It is also a compact integration test for the framework's ideas.
For the smallest example:
python -m hyperclass examples.counter:app
Principles
- Python is the authoring language. Control flow, composition, inheritance, validation, and reuse are ordinary Python.
- The browser remains the browser. Hyperclass emits standard HTML and CSS rather than recreating the DOM on the server.
- Classes mean classes. Python inheritance has a visible relationship to HTML classes and the CSS cascade.
- HTTP is the state boundary. There is no hydration protocol or hidden client component lifecycle.
- Output should be boring. Generated markup stays readable in View Source and DevTools.
- Small is a feature. Prefer the standard library, WSGI, and a pinned htmx asset over a framework stack.
Status
Hyperclass is deliberately pre-alpha: useful enough to build small applications and young enough for its API to change. Python 3.10 through 3.14 are tested.
Apache-2.0 licensed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hyperclass-0.0.5.tar.gz.
File metadata
- Download URL: hyperclass-0.0.5.tar.gz
- Upload date:
- Size: 28.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c7512c82c3639136b2f4adbbd3440545598be6e6c71dd7777f3103be3ce6ccd1
|
|
| MD5 |
a4f3249760e8c363350692eb367ecfc6
|
|
| BLAKE2b-256 |
9d0b5b8a121693f64e0402a7fd1ab980b8771b5e27ddf198501cc75dfd331ee7
|
Provenance
The following attestation bundles were made for hyperclass-0.0.5.tar.gz:
Publisher:
release.yml on grantjenks/python-hyperclass
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hyperclass-0.0.5.tar.gz -
Subject digest:
c7512c82c3639136b2f4adbbd3440545598be6e6c71dd7777f3103be3ce6ccd1 - Sigstore transparency entry: 2655492798
- Sigstore integration time:
-
Permalink:
grantjenks/python-hyperclass@6cfc8e1e7600469217c7e800d1858a3d156fc1e7 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/grantjenks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6cfc8e1e7600469217c7e800d1858a3d156fc1e7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file hyperclass-0.0.5-py3-none-any.whl.
File metadata
- Download URL: hyperclass-0.0.5-py3-none-any.whl
- Upload date:
- Size: 21.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b38c93b46419f1b5d1c8a2e2d36751156a5148da0d63f27ddd062ecf9b89e735
|
|
| MD5 |
956e1adb6ec0c429e2d68783abd8e7e1
|
|
| BLAKE2b-256 |
da81f60019053c93ecb95e8e746fad91fb9e01c9060c1429cb19c87d546d8c64
|
Provenance
The following attestation bundles were made for hyperclass-0.0.5-py3-none-any.whl:
Publisher:
release.yml on grantjenks/python-hyperclass
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hyperclass-0.0.5-py3-none-any.whl -
Subject digest:
b38c93b46419f1b5d1c8a2e2d36751156a5148da0d63f27ddd062ecf9b89e735 - Sigstore transparency entry: 2655492801
- Sigstore integration time:
-
Permalink:
grantjenks/python-hyperclass@6cfc8e1e7600469217c7e800d1858a3d156fc1e7 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/grantjenks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6cfc8e1e7600469217c7e800d1858a3d156fc1e7 -
Trigger Event:
push
-
Statement type: