htpy - HTML in Python

repository·main·Indexed 19 days ago

https://github.com/pelme/htpy

A library for writing HTML directly in Python without a separate template language. It uses native Python syntax and the `[]` operator to create structured, type-safe, and extensible HTML components. htpy is framework-agnostic, supporting Django, Flask, Starlette, and FastAPI, and provides full support for asynchronous rendering and async iterators via `.aiter_chunks()`.

Tokens
13.5K
Snippets
52
Records
65
Agent score
64%

What's inside htpy

  1. Key features and benefits of htpy

    main

    htpy is designed to be a lightweight, Pythonic way to generate HTML with several advantages:

    • Type Safety: Compatible with mypy and pyright for static type checking.
    • Debugging: Since it is plain Python, you can use standard Python debuggers instead of debugging template engine errors.
    • Extensibility: You can extend functionality using regular Python functions rather than learning custom template tags or filters.
    • Framework Agnostic: Works with Django, Flask, Starlette, FastAPI, and other web frameworks.
    • Async Support: Full support for asynchronous rendering, making it suitable for ASGI frameworks.
    • Component-Based: You can create reusable components, snippets, and layouts using standard Python variables, functions, or classes.
    • HTMX Friendly: Well-suited for writing server-rendered partials and components for htmx.
  2. Understand how `from htpy import whatever_element` works

    main

    The ability to import any HTML element directly from the htpy module (e.g., from htpy import div, span, p) is enabled by Python's module-level __getattr__ mechanism (introduced in Python 3.7).

    When you attempt to import an element that is not explicitly defined in the module, htpy uses this mechanism to dynamically create and return new Element instances for the requested tag names.

  3. Combine @with_children with other decorators

    main

    You can combine @with_children with other decorators, such as context consumers.

    Important: The order of decorators and arguments is the inverse of the source code order. The innermost decorator is the first to wrap the function and maps to the first argument.

    Example order: If @with_children is the outermost decorator and @theme_context.consumer is the innermost, the function signature should be def my_component(theme, children, *, ...).

    from typing import Literal
    from htpy import Context, Node, Renderable, div, h1, with_children
    
    Theme = Literal["light", "dark"]
    theme_context: Context[Theme] = Context("theme", default="light")
    
    @with_children
    @theme_context.consumer
    def my_component(theme: Theme, children: Node, *, extra: str) -> Renderable:
        ...
  4. Why htpy uses standard Python syntax instead of JSX-like tags

    main

    Unlike JSX or pyxl, htpy does not use angle-bracket syntax for HTML tags. This design choice ensures full compatibility with standard Python tooling, including:

    • Code formatters (e.g., Black, Ruff)
    • IDE editors
    • Static type checkers (e.g., Mypy, Pyright)

    By using standard Python function calls and syntax, htpy integrates seamlessly into existing Python development workflows without requiring custom language extensions.

  5. How htpy syntax works

    main
    htpy uses the __getitem__ method (the [] syntax) to specify child elements of an HTML tag. This design choice explicitly separates HTML attributes from child elements, making the code more readable and preventing confusion between the two.
  6. Pass data through subtrees using Contexts

    main

    Contexts in htpy allow you to pass data deep into a component tree without manually passing arguments through every intermediate component. This is conceptually similar to React Context.

    Workflow:

    1. Define: Create a context object using Context(name[, *, default]).
    2. Provide: Use my_context.provider(value, children) to set a value for a specific subtree.
    3. Consume: Decorate a component function with @my_context.consumer. The context value will be injected as the first argument to the decorated function.

    Key Properties:

    • Type Safety: The Context class is generic and supports static type checking.
    • No Global State: Values are passed as part of the rendering tree.
    • Nesting: You can nest multiple providers; different subtrees can use different values.
    • Multiple Contexts: A component can consume multiple contexts by stacking decorators. The arguments will be passed in the order the decorators are applied (bottom-to-top/inside-out logic, but effectively the decorated function receives them as arguments).

    Note: When using multiple decorators, the order of arguments in your function must match the order of the decorators.

    from typing import Literal
    from htpy import Context, Node, div, h1
    
    Theme = Literal["light", "dark"]
    
    # 1. Define the context
    theme_context: Context[Theme] = Context("theme", default="light")
    
    def my_page() -> Node:
        # 2. Provide the value for a subtree
        return theme_context.provider(
            "dark",
            div[
                h1["Hello!"],
                sidebar("The Sidebar!"),
            ],
        )
    
    # 3. Consume the context
    @theme_context.consumer
    def sidebar(theme: Theme, title: str) -> Node:
        return div(class_=f"theme-{theme}")[title]
    
    print(my_page())
  7. Stream HTML content using generators and callables

    main

    Because htpy is built with generators, it supports incremental page generation. This allows you to stream HTML to a client (e.g., via a web server) while data is still being fetched or processed. This improves perceived performance by allowing the browser to start parsing the <head> (including CSS) while the server continues to generate the body.

    To enable streaming, you must avoid heavy upfront work and instead use lazy constructs like generators or callables as children of your elements.

  8. Basic syntax for creating HTML elements

    main

    In htpy, HTML elements are imported directly from the htpy module.

    • Attributes are specified by calling the element: element(attr="value").
    • Children are specified using square brackets: element[children].

    Elements can be arbitrarily nested by combining these two syntaxes.

    from htpy import div, article, section, p
    
    # Creating an element with an attribute and a child
    print(div(id="hi")["Hello!"])
    # <div id="hi">Hello!</div>
    
    # Nesting elements
    print(section[article[p["Lorem ipsum"]]])
    # <section><article><p>Lorem ipsum</p></article></section>
  9. Create components as plain Python functions

    main

    In htpy, a component is simply a plain Python function that returns a htpy element (a Renderable). There is no need for special classes or decorators to create a basic component.

    Immutability Note: All elements in htpy are immutable. You cannot modify an existing element after creation. Instead, you should create component functions that accept arguments to generate customized elements.

  10. Handle text and prevent XSS with automatic escaping

    main

    You can pass strings directly as children of an element. htpy automatically escapes strings to prevent XSS vulnerabilities. This makes it safe to use Python f-strings to inject variable data directly into your markup.

    from htpy import h1
    
    user_supplied_name = "bobby </h1>"
    print(h1[f"hello {user_supplied_name}"])
    # <h1>hello bobby &lt;/h1&gt;</h1>
  11. Quickstart: Define and render HTML in Python

    main

    htpy allows you to define HTML structures using Python objects and the [] syntax to specify child elements. This approach avoids template languages and allows you to use standard Python logic (like list comprehensions and loops) to generate content.

    To use it, import the desired HTML tags (e.g., html, body, h1, ul, li) from htpy and nest them using square brackets.

    from htpy import body, h1, head, html, li, title, ul
    
    menu = ["egg+bacon", "bacon+spam", "eggs+spam"]
    
    # Define the structure using [] for children
    # Attributes (like classes) are passed before the []
    print(
        html[
            head[title["Today's menu"]],
            body[
                h1["Menu"],
                ul(".menu")[(li[item] for item in menu)],
            ],
        ]
    )
  12. Iterate over children using lists or generators

    main

    To generate multiple children, pass a list, tuple, or generator to the element's square brackets.

    Warning on Generators: Generators are lazily evaluated and can only be consumed once. If you attempt to render an element containing a generator a second time, it will raise a RuntimeError: Generator has already been consumed. If you need to render the same content multiple times, use a list instead.

    from htpy import ul, li, div, img
    
    # Using a generator (lazy evaluation)
    print(ul[(li[letter] for letter in "abc")])
    # <ul><li>a</li><li>b</li><li>c</li></ul>
    
    # Using a list (safe for multiple renders)
    my_images = [img(src="a.jpg"), img(src="b.jpg")]
    print(div[my_images])
    # <div><img src="a.jpg"><img src="b.jpg"></div>