Skip to content

django-div

Build and parse HTML in Python with Pydantic models.

from django_div import A, Div, P

print(Div(P("Hello, World!"), A("Click", href="/x"), class_="card"))
<div class="card"><p>Hello, World!</p><a href="/x">Click</a></div>

Children are positional, attributes are keyword arguments. Text is escaped, void elements self-close, and Python attribute spellings map onto HTML ones.

Why this exists

Most HTML-in-Python libraries build markup and stop there. Here the tree is a Pydantic model, which means the same objects can go in four directions:

  • Build

    Compose elements as Python values, with escaping handled for you.

    Building HTML

  • Parse

    Read existing markup back into the same tree, then search and edit it.

    Parsing HTML

  • Serialize

    Round-trip a page through JSON with its element classes intact.

    Serializing

  • Render in Django

    Components as templates, with escaping that works both ways.

    Django

Install

With uv, create a virtual environment if needed and install the package:

uv venv
uv pip install django-div

With pip, install into your active virtual environment:

python -m pip install django-div

For uv-managed projects, uv add records the dependency in pyproject.toml. The same extras below also work with uv pip install and python -m pip install:

uv add django-div            # building only
uv add 'django-div[parse]'   # plus from_html()/parse(), via bs4 + lxml
uv add 'django-div[html5]'   # spec-exact parsing, ~3x slower than lxml
uv add 'django-div[markdown]' # plus from_markdown(), via markdown-it-py

django-div needs Python 3.12 or later. Pydantic is the only hard dependency. Parsers are optional and their imports are guarded, so building HTML pulls in nothing else. Django is optional too: only django_div.django imports it.

A tour in one page

from django_div import Div, H1, Li, P, Span, Ul, from_html

# Nesting, attributes, escaping
Div(H1("Title"), P("a < b"), class_="page")
# <div class="page"><h1>Title</h1><p>a &lt; b</p></div>

# None and False children drop out, so inline conditionals work
Div("Hello", user and Span(user.name))

# Collections flatten, so comprehensions splat in
Ul(Li(item) for item in items)

# Parse markup into the same kind of tree
page = from_html('<div><a href="/x" target="_blank">Click</a></div>')
page.find("a").attrs["href"]        # '/x'
page.text                           # 'Click'

# Edit it and render it back out
for link in page.find_all("a", target="_blank"):
    link.attrs["rel"] = "noopener"
print(page)

Where to next

The four cards above cover building, parsing, serializing, and Django. Beyond those:

Reusable elements and browser data

Derive element variants with with_attrs(), render explicit ARIA states from Python booleans, and pass browser data with JsonScript. The cookbook includes button variants, disclosures, HTML templates, and JSON data recipes.

Search by class tokens or custom conditions with predicates, and use transform() to replace, remove, or unwrap nodes on a copied tree. See searching and transforming a copy for traversal and copy semantics.

llms.txt

This documentation is available in the llms.txt format, a Markdown convention suited to LLMs and AI coding assistants.

Two files are published:

  • llms.txt: a short description of the project plus links to each section. The structure is described here.
  • llms-full.txt: the same index with the content of every page inlined.

Every page is also published as Markdown alongside its HTML, so you can point an assistant at a single section rather than the whole corpus. Append .md to the page name:

https://django-div.readthedocs.io/en/latest/building.md
https://django-div.readthedocs.io/en/latest/django.md

Where it came from

django-div started as five throwaway scripts trying to answer one question: how do you make Div("hello", class_="x") work when Pydantic wants keyword fields? Those experiments are still in the git history at the Baseline: original django-div demo experiments commit.

Prior art

htpy, dominate, and django-components cover adjacent ground, and htpy in particular landed on a very similar constructor shape. django-div's angle is the Pydantic model underneath: the same objects parse, validate, and serialize.