The browser DOM, in Python.
Generate HTML. Parse real pages. Query with CSS or XPath. Manipulate a browser-style DOM.
User and learn real HTML, DOM and JavaScript-style APIs using Python code!
domonic is a pure-Python DOM.
from domonic.html import *
page = html(
body(
h1("Hello, World!"),
p("HTML as Python objects."),
a("GitHub", _href="https://github.com")
)
)
print(page)<html><body><h1>Hello, World!</h1><p>HTML as Python objects.</p><a href="https://github.com">GitHub</a></body></html>But generating HTML is only the beginning.
heading = page.querySelector("h1")
heading.textContent = "Hello, DOM!"
for link in page.querySelectorAll("a"):
print(link.href)The same kind of DOM can also come from parsed HTML.
from domonic import domonic
document = domonic.parseString("""
<html>
<body>
<h1>Hello</h1>
<a href="/api/v1/web-embed/proxy?proxyUrl=https%3A%2F%2Fgithub.com%2Fdocs&mode=full">Documentation</a>
</body>
</html>
""")
print(document.querySelector("h1").textContent)from domonic.bs4 import BeautifulSlop
soup = BeautifulSlop("<main><a href='/api/v1/web-embed/proxy?proxyUrl=https%3A%2F%2Fgithub.com%2Fdocs&mode=full'>Docs</a></main>", "html.parser")
for link in soup.find_all("a", href=True):
print(link.text, link["href"])
# Same object, still domonic:
print(soup.querySelector("a").getAttribute("href"))from domonic.html import a, article, h2, p
def card(title, body, href):
return article(h2(title), p(body), a("Open", _href=href), _class="card")
print(card("Python DOM", "Generate HTML with Python objects.", "/docs"))from fastapi.responses import StreamingResponse
from domonic.html import body, html, table, td, tr
def rows():
for index in range(50000):
yield tr(td(f"Row {index}"), td(f"Data {index}"))
page = html(body(table(rows())))
return StreamingResponse(page.stream(), media_type="text/html")from domonic.webapi.sanitizer import Sanitizer
clean = Sanitizer().sanitizeToString(
'<p onclick="bad()">Hello <script>bad()</script></p>'
)
print(clean)from domonic.diffdom import DiffDOM
from domonic.html import div, p
old = div(p("Version one"))
new = div(p("Version two"))
changes = DiffDOM().diff(old, new)
print(changes)More focused guides:
- Examples gallery
- Scrape HTML with Python
- Server-side HTML
- Compiled SSR: decorators, caching and internals
- Live DOM updates
- Parser performance
| 🏗️ Markup generation | HTML5, SVG, XML, MathML, RSS, Atom, ODF, A-Frame, X3D and custom elements |
| 🌳 DOM | Document, Element, Node, NodeList, fragments, ranges, events, traversal, observers, shadow DOM and more |
| 🔎 Querying | CSS selectors and XPath |
| 📥 Parsing | Multiple interchangeable parser backends |
| 🌐 Web APIs | URL, URLPattern, storage, messaging, workers, crypto, performance, permissions and more |
| 🟨 JavaScript-like APIs | Array, Date, Math, String, Number, Promise, timers, typed arrays and JSON helpers |
| ⚡ CLI | Query URLs, files or piped HTML with CSS and XPath |
| 🧪 Experiments | dQuery, d3-inspired utilities, diffdom, BeautifulSlop and other browser-inspired ideas |
Not every API is implemented to browser-complete parity — the goal is to keep
moving closer to the real standards. Python 3.10+.
python3 -m pip install domonicUpgrade:
python3 -m pip install --upgrade domonicFor the domonic command line tool, pipx keeps the executable isolated
and on your shell path:
brew install pipx
pipx ensurepath
pipx install domonic
domonic -x https://example.com '//title'Then:
from domonic.html import *
print(h1("hello world"))HTML elements are ordinary Python objects.
The tag names are the HTML names. The attribute names are the HTML names with a Python-friendly leading underscore where needed. That means examples often read like HTML with Python syntax:
from domonic.html import *
card = div(
h2("domonic"),
p("The browser DOM, in Python."),
a("Documentation", _href="https://domonic.readthedocs.io"),
_class="card"
)
print(card)Attributes are prefixed with _ to avoid collisions with Python keywords:
label("Email", _for="email", _class="label")<label for="email" class="label">Email</label>For attributes that cannot be expressed as Python identifiers:
div(
"hello",
**{"_data-user-id": "42"}
)domonic elements are more than formatted strings.
They're nodes in a document tree.
from domonic.html import *
from domonic.dom import document
page = html(
body(
main(
h1("Projects"),
ul(
li("domonic"),
li("Blueberry"),
li("ezcron")
)
)
)
)
print(page.querySelector("h1"))
print(page.querySelectorAll("li"))Manipulate the tree using familiar DOM concepts:
title = page.querySelector("h1")
title.textContent = "Open source projects"
new_item = document.createElement("li")
new_item.textContent = "something new"
page.querySelector("ul").appendChild(new_item)The project aims to follow the real platform where practical:
See the DOM documentation for the implemented API.
Use browser-style selectors directly against the tree.
page.querySelector("button")
page.querySelector("#content")
page.querySelector(".active")
page.querySelectorAll("a")
page.querySelectorAll("a[rel=nofollow]")
page.querySelectorAll("a[href='#services']")
page.querySelectorAll("a[href$='technology']")
page.querySelectorAll("a[href*='github']")for link in page.querySelectorAll("a"):
print(link.href)XPath is available too.
From Python:
from domonic import domonic
page = domonic.parseString("<main><h1>Hello</h1></main>")
# use XPath against your document treeOr straight from your terminal:
domonic -x https://example.com '//a'Against a local file:
domonic --xpath-file ./page.html '//title'Or pipe HTML directly into it:
curl -s https://example.com | domonic -x '//a' --countfrom domonic import domonic
page = domonic.parseString("""
<!doctype html>
<html>
<body>
<article>
<h1>Hello from HTML</h1>
</article>
</body>
</html>
""")
print(page.querySelector("h1"))You can also load a page through the window API:
from domonic.window import window
window.location = "https://example.com"
print(window.document.title)One parser does not fit every job.
domonic lets you choose between zero dependencies, pure Python compatibility, malformed-HTML repair and high-performance native parsers.
from domonic import domonic
page = domonic.parseString("<p>Hello</p>", parser="selectolax")
page = domonic.parseString("<p>Hello</p>", parser="turbohtml")
page = domonic.parseString("<p>Hello</p>", parser="lxml_html")
page = domonic.parseString("<p>Hello</p>", parser="markupever")
page = domonic.parseString("<p>Hello</p>", parser="html5_parser")
page = domonic.parseString("<p>Hello</p>", parser="html.parser")
page = domonic.parseString("<p>Hello</p>", parser="html5lib")
page = domonic.parseString("<p>Hello</p>", parser="expat")
page = domonic.parseString("<p>Hello</p>", parser="justhtml")The default is parser="auto", which picks the fastest installed backend that
can parse the input (trying selectolax, turbohtml, lxml_html,
html5_parser, markupever, html.parser, justhtml, then html5lib). Call
domonic.get_active_parser() afterwards to see which one ran.
Set one for your application:
from domonic import domonic
domonic.set_default_parser("html.parser")
page = domonic.parseString("<p>Hello</p>")Available parser backends:
| Parser | Why use it? |
|---|---|
tl |
Optional Rust tl-parser with direct raw DOM adaptation (Python 3.12+) |
reliq |
Optional native parser with direct DOM adaptation; explicitly selected |
selectolax |
Fast native HTML parsing with direct domonic DOM adaptation |
turbohtml |
Fast native WHATWG parsing with direct domonic DOM adaptation |
lxml_html |
Very fast lxml-backed parsing and direct lxml DOM adaptation |
html5_parser |
Fast HTML5 parsing through the shared lxml DOM adapter |
markupever |
Fast Rust-powered HTML repair; uses the shared lxml DOM adapter |
html.parser |
Python standard library; no extra dependency |
justhtml |
Pure-Python alternative with a direct domonic DOM adapter |
html5lib |
Pure Python and bundled with domonic |
expat |
Built into Python; useful for XML-like input |
Optional parsers require their respective packages.
The Rust tl-parser backend is available with python -m pip install tl-parser
and domonic.parseString(markup, parser="tl") (Python 3.12+ for the tested
0.7.12 release). It builds raw domonic nodes directly through the public API
and remains outside auto. It preserves native tl recovery behavior, which
is not HTML5 parsing: markup-like content inside script/style elements can be
interpreted as tags. The adapter preserves leading PUBLIC/SYSTEM doctypes
separately because tl does not expose doctype nodes.
Reliq can be tried with python -m pip install reliq and
domonic.parseString(markup, parser="reliq"). It converts the native tree directly
without serializing and reparsing HTML. It preserves Reliq's parsing semantics;
malformed HTML can produce a different tree from HTML5 backends. It is not in
the auto cascade. Benchmark it locally with
python scripts/benchmark_parsers.py --parsers reliq selectolax turbohtml.
The optimized native-array adapter currently targets Reliq 0.0.48; other
versions use the public-API adapter. For separate native parsing and conversion
timings, run python scripts/benchmark_reliq.py.
Install the native parser stack like this:
python -m pip install selectolax
python -m pip install turbohtml
python -m pip install lxml
python -m pip install markupever lxml
python -m pip install html5-parser lxmlFor parser details and installation notes, see the parser performance guide.
Every element can be rendered with str():
from domonic.html import *
page = div(
h1("Hello"),
p("Rendered from a Python DOM.")
)
markup = str(page)
print(markup)Write documents to disk with render:
render(f"{page}", "index.html")Rendering behaviour can be configured through DOMConfig.
from domonic.dom import DOMConfig
print(DOMConfig.GLOBAL_AUTOESCAPE)
print(DOMConfig.RENDER_OPTIONAL_CLOSING_TAGS)See the DOM documentation for all rendering options.
domonic includes a large practical slice of JavaScript's familiar APIs.
from domonic.javascript import Math, Array, Date
print(Math.random())
numbers = Array(1, 2, 3)
print(numbers.splice(1))from domonic.javascript import URL
url = URL("https://example.com:8000/blog/article#hello")
print(url.protocol)
print(url.host)
print(url.port)
print(url.pathname)
print(url.hash)Timers are there too:
from domonic.javascript import setTimeout
def hello():
print("hello")
setTimeout(hello, 1000)Other APIs include things such as:
String · Number · Promise · JSON · typed arrays · timers · URL helpers · global functions
See the JavaScript documentation for the full surface.
The web platform is much bigger than the DOM.
domonic implements or experiments with Python versions of APIs including:
URLURLSearchParamsURLPattern- Fetch / XHR helpers
- Web Storage
- Cookie Store
- History
- File API
- Web Crypto
- Web Workers
- WebSocket
- Server-Sent Events
- Messaging
- Permissions
- Notifications
- Performance APIs
- Scheduler /
postTask - Sanitizer
- Compression streams
- Canvas / WebGL
- CSS font loading
- Gamepad
- Media APIs
- Import maps
- Speculation rules
- Custom elements
- Shadow DOM
- Mutation / tree observation
- XPath
…and more.
The README deliberately doesn't try to document all of them.
The DOM isn't only HTML.
domonic can build other document types using the same object-oriented approach.
from domonic.html import *
from domonic.svg import *
icon = svg(
circle(
_cx="50",
_cy="50",
_r="40",
_stroke="green",
_fill="yellow"
),
_width="100",
_height="100"
)
print(icon)There is also support for XML, MathML, RSS, Atom and ODF, sitemaps, A-Frame and X3D, and custom elements.
DOM-style property access works too.
from domonic.html import *
box = div("hello", _id="message")
box.style.backgroundColor = "black"
box.style.fontSize = "12px"
print(box)<div id="message" style="background-color: black; font-size: 12px;">hello</div>Yes, there is also a jQuery-inspired API.
Because apparently implementing the DOM wasn't enough.
from domonic.html import *
from domonic.dQuery import º
page = html(
body(
li(_class="thing"),
div(_id="test")
)
)
print(º("#test"))
print(º(".thing"))Append nodes:
new_div = º('<div class="child"></div>')
º("#test").append(new_div)dQuery is useful in its own right, but it also serves as a demanding consumer of the underlying DOM implementation.
See the dQuery documentation for the full API.
domonic also contains a Python port / interpretation of useful parts of the d3 ecosystem built on top of its JavaScript and DOM layers.
from domonic.d3 import *See the d3 documentation for current coverage.
domonic includes BeautifulSlop, a BS4-style compatibility experiment built over the domonic parsing system.
It exists for code that wants familiar soup-like ergonomics while still landing in the domonic world.
See the BeautifulSlop documentation for current compatibility.
Convert Python data to JSON:
from domonic.decorators import as_json
@as_json
def response():
return {
"hello": "world",
"items": [1, 2, 3]
}
print(response())JSON arrays can also be turned into HTML tables or CSV:
import domonic.JSON as JSON
data = [{"id": "01", "name": "some item"}]
table = JSON.tablify(data)
JSON.csvify(data, "data.csv")And CSV can go the other way:
data = JSON.csv2json("data.csv")There is a small tweening library too.
from domonic.lerpy.easing import *
from domonic.lerpy.tween import *
position = {
"x": 0,
"y": 0,
"z": 0
}
tween = Tween(
position,
{"x": 10, "y": 5, "z": 3},
6,
Linear.easeIn
)
tween.start()domonic even contains Python wrappers around common command-line tools on Unix-like systems:
from domonic.terminal import *
print(ls())
print(pwd())
print(git("status"))
print(df())Or run an arbitrary command:
from domonic.terminal import command
command.run("echo hello")Windows users can use domonic.cmd. See the terminal documentation for more.
domonic comes with a CLI for working with HTML without writing a script.
Install it as a standalone command with pipx:
brew install pipx
pipx ensurepath
pipx install domonic
domonic -x https://example.com '//title'domonic -hdomonic -vdomonic -q https://example.com 'a'
domonic -q https://example.com 'a' --parser selectolaxdomonic -x https://example.com '//a'
domonic -x https://example.com '//a' --parser selectolaxdomonic -q https://example.com 'h1' --textdomonic -q https://example.com 'a' --attr hrefdomonic -q https://example.com 'a' --firstdomonic -x https://example.com '//a' --countdomonic --xpath-file ./page.html '//title'
domonic --query-file ./page.html 'a.cta' --parser selectolaxcurl -s https://example.com | domonic -x '//a' --count
cat page.html | domonic -q 'a.cta' --attr href --parser selectolaxdomonic -e 'html(head(), body(h1("hello")))'domonic -p myprojectChoose a server:
domonic -p myproject --server fastapiBecause domonic elements are Python objects that render to markup, they work naturally in Python web applications.
The repository contains examples for frameworks including:
- FastAPI
- Flask
- Django
- Sanic
…and others. See the servers documentation for framework-specific snippets.
Use @compiled when a view only needs to return HTML. It compiles at definition/import time; supported calls render strings without constructing a DOM on every request.
from domonic import compiled
from domonic.dom import DOMConfig
from domonic.html import div, h1, p
DOMConfig.GLOBAL_AUTOESCAPE = True
@compiled
def home(name="World"):
return div(h1("Hello"), p(name))
print(home("Alice & Bob"))
# <div><h1>Hello</h1><p>Alice & Bob</p></div>The decorator wraps the explicit renderer = domonic.compile(view) API. Memory-only is the default; @compiled(strict=True, cache_dir="/tmp/domonic") adds a private disk bytecode cache. Startup prepares the renderer, so the first request needs no compilation or warmup. Request values are evaluated afresh each time.
Under the hood, inspect retrieves the function source and an AST pass recognizes standard domonic tag calls. Static markup becomes string literals; dynamic expressions become inserts in a generated Python renderer. home.source shows that code, and home.__original__ retains the original view. Unsupported views use normal rendering unless strict=True requires compilation.
Text escaping follows DOMConfig.GLOBAL_AUTOESCAPE, enabled above; attributes are escaped. Use raw(trusted_html) from domonic.html to explicitly insert trusted HTML child content. It does not sanitize input or disable attribute escaping.
Try the tiny standalone example with python -m examples.ssr.decorated_view, or the FastAPI server example with python -m examples.ssr.compiled_views. The server compiles its views during startup before accepting requests. Run python scripts/benchmark_ssr.py --repeats 5 to compare cold first renders.
See the compiled-rendering guide for supported syntax, route integration, DOM snapshots, cache behavior, benchmarks, and how the compiler works.
There are working examples throughout the repository:
👉 github.com/byteface/domonic/tree/master/examples
Some projects built using domonic:
Extends domonic to have even further capabilities!
A JavaScript interpreter in pure Python!
A browser-based file OS and an example of building components with domonic.
A cron viewer.
A small game.
A lightweight, low-dependency DOM-focused relative of domonic.
Use the docs for detailed API coverage, package-specific examples and less common functionality.
Useful links:
Clone the repository and install the development dependencies:
python3 -m pip install -r requirements-dev.txtRun the test suite:
make testOr:
pytest testsRun an individual module:
python -m unittest tests.test_htmlCoverage:
coverage run -m unittest discover tests/
coverage reportThe tests are also useful as executable examples of the API.
Contributions are welcome.
- Fork the repository
- Create a branch
- Make your change
- Add or update tests where appropriate
- Open a pull request
See CONTRIBUTING.md for more information.
⭐ If you find it useful, consider starring the project.
Documentation · PyPI · Examples · Releases