# django-div > Build and parse HTML in Python with Pydantic models. --- # django-div Build and parse HTML in Python with Pydantic models. ```python from django_div import A, Div, P print(Div(P("Hello, World!"), A("Click", href="/x"), class_="card")) ``` ```html

Hello, World!

Click
``` 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](building/) - **Parse** Read existing markup back into the same tree, then search and edit it. [ Parsing HTML](parsing/) - **Serialize** Round-trip a page through JSON with its element classes intact. [ Serializing](serializing/) - **Render in Django** Components as templates, with escaping that works both ways. [ Django](django/) ## Install With uv, create a virtual environment if needed and install the package: ```console uv venv uv pip install django-div ``` With pip, install into your active virtual environment: ```console 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`: ```console 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 ```python from django_div import Div, H1, Li, P, Span, Ul, from_html # Nesting, attributes, escaping Div(H1("Title"), P("a < b"), class_="page") #

Title

a < b

# 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('
Click
') 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: - [Markdown](markdown/): render the tree as Markdown, read Markdown in - Cookbooks: [HTML](cookbook/) · [Django](django-cookbook/) · [Markdown](markdown-cookbook/) - [API reference](reference/): every function, class, and constant - [Contributing](contributing/): setup, conventions, tests ## 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](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](parsing/#searching) and [transforming a copy](parsing/#transforming-a-copy) for traversal and copy semantics. ## llms.txt This documentation is available in the [llms.txt](https://llmstxt.org/) format, a Markdown convention suited to LLMs and AI coding assistants. Two files are published: - [`llms.txt`](https://django-div.readthedocs.io/en/latest/llms.txt): a short description of the project plus links to each section. The structure is described [here](https://llmstxt.org/#format). - [`llms-full.txt`](https://django-div.readthedocs.io/en/latest/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: ```text 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](https://htpy.dev), [dominate](https://github.com/Knio/dominate), and [django-components](https://github.com/django-components/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. --- # Building HTML Every HTML element has a class, named after the tag with a capital letter: `Div`, `P`, `H1`, `Textarea`. The set covers all 138 elements MDN tracks -- the 113 current ones in the [WHATWG living standard](https://html.spec.whatwg.org/multipage/indices.html#elements-3) plus 19 deprecated and 6 experimental, which warn when you build one -- each documented on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element). Children are positional arguments, attributes are keyword arguments. ```python from django_div import A, Div, H1, P Div( H1("Welcome"), P("A paragraph with ", A("a link", href="/docs"), "."), class_="page", ) ``` ```html

Welcome

A paragraph with a link.

``` Rendering happens on `str()`, so `print(tag)`, f-strings, and `"".join(...)` all work directly. ## Attributes Python spellings are translated to HTML ones: a trailing underscore is dropped, and remaining underscores become hyphens. | Python | HTML | | --- | --- | | `class_="card"` | `class="card"` | | `for_="email"` | `for="email"` | | `data_test_id="hero"` | `data-test-id="hero"` | | `aria_label="Close"` | `aria-label="Close"` | | `hx_get="/rows"` | `hx-get="/rows"` | | `http_equiv="refresh"` | `http-equiv="refresh"` | ### Reserved words The trailing underscore also covers every Python keyword. Add one underscore to the end of the keyword, and the rule removes it again on render. | Python | HTML | Where it applies | | --- | --- | --- | | `class_` | `class` | every element | | `for_` | `for` | `