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="/docs">Documentation</a>
</body>
</html>
""")
print(document.querySelector("h1").textContent)from domonic.bs4 import BeautifulSlop
soup = BeautifulSlop("<main><a href='/docs'>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:
| 🏗️ 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>")Fastest on the bundled large-page benchmark first:
| Parser | Why use it? |
|---|---|
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.
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.
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