urllib.parse is the module every Python dev reaches for to parse a URL,
split out its host and query string, and rebuild it with a parameter
changed. As of mid-2026 the Mojo ecosystem has no equivalent, so mojo-url
fills that gap: same function names, same arguments, byte-for-byte
matching output, so a Python developer can pick it up without a second
manual.
If you know Python's urllib.parse, the names line up almost exactly:
Python (urllib.parse) |
mojo-url |
|---|---|
r = urlparse(url) |
var r = urlparse(url) |
r.scheme / r.netloc / r.path |
r.scheme / r.netloc / r.path |
parse_qsl(r.query) |
parse_qsl(r.query) |
quote(s) / unquote(s) |
quote(s) / unquote(s) |
urljoin(base, rel) |
urljoin(base, rel) |
One shape difference: urlencode takes a List[QueryPair] here (not a dict),
so build pairs with QueryPair(key, value) and pass the list to
urlencode(pairs).
urlparse/urlunparse: the six-component split (scheme://netloc/path;params?query#fragment), with netloc-derivedusername(),password(),hostname(), andport()accessors. IPv6 literals ([::1]), IPv6 zone IDs, userinfo, and non-numeric or absent ports are all handled the way CPython'surlsplithandles them.quote/quote_plus/unquote/unquote_plus: RFC 3986 percent-encoding, UTF-8 aware in both directions, with invalid%XXsequences recovered as U+FFFD on decode (errors="replace") and the/-safe-by-default vs.+-for-space conventions ofquoteandquote_pluskept distinct.urlencode/parse_qs/parse_qsl: build a query string from pairs, or parse one back into ordered pairs or a{key: [values...]}grouping, with repeated keys and blank-value dropping matchingurllib.parsedefaults.urljoin: RFC 3986 Section 5 reference resolution asurllib.parseimplements it (including CPython's backward-compatible handling of a same-scheme reference), passing the canonical Section 5.4 conformance table 42/42.
- WHATWG-URL normalization or validation. This mirrors
urllib.parsesemantics, not the browser URL Standard: no host normalization, no idna, no scheme-specific validation beyond whaturllib.parseitself does. - Return
(key, value)tuples from query parsing.parse_qslreturnsList[QueryPair]instead, because Mojo'sListcan't hold Python-style anonymous tuples;QueryPairhas the same.key/.valuefields a tuple unpack would give you. - Diverge from
urljoin's scheme handling. A reference whose scheme differs from the base (g:h) is treated as absolute, exactly as CPython does. A reference whose scheme equals the base scheme (http:gagainst anhttpbase) resolves relatively — CPython's long-standing backward-compatibility behavior — rather than the strict-RFChttp:g. Digit-led pseudo-schemes (10:30.html) are not schemes at all, so they resolve as relative paths.
With pixi:
pixi install
pixi run test
pixi run demoOr with uv:
uv venv
uv pip install mojo --index https://whl.modular.com/nightly/simple/ --prerelease allow
.venv/bin/mojo run -I src test/test_url.mojoRequires a Mojo nightly (>=1.0.0b3).
from url import (
urlparse, urlunparse, quote, quote_plus, unquote, unquote_plus,
urlencode, parse_qs, parse_qsl, urljoin, ParseResult, QueryPair,
)
def main() raises:
var r = urlparse(String("https://user:pass@host.example.com:8080/a/b?c=d#e"))
print(r.scheme, r.hostname(), r.port()) # https host.example.com 8080
print(quote(String("café / a"))) # caf%C3%A9%20/%20a
print(unquote(String("caf%C3%A9"))) # café
var grouped = parse_qs(String("a=1&a=2&b=3"))
print(grouped["a"]) # [1, 2]
print(urljoin(String("http://a/b/c/d;p?q"), String("../../g"))) # http://a/gpixi run fixtures # regenerate test/data/fixtures.txt from CPython
pixi run test38 tests: hand-written behavioral checks per function, the RFC 3986
Section 5.4 conformance table (42/42), and a fixture harness that
byte-matches all 173 urllib.parse fixtures generated fresh from
CPython. test/fuzz_runner.mojo is the robustness target: 600 mutated
inputs through urlparse/urlunparse and both quote/unquote pairs,
zero crashes and zero hangs.
Eleven pure-Mojo libraries that mirror familiar Python stdlib and PyPI APIs, filling gaps in the native Mojo ecosystem:
- mojo-xml — general-purpose XML
parsing, an ElementTree-shaped DOM (Python's
xml.etree.ElementTree) - mojo-feed — RSS, Atom, and
JSON Feed parsing (Python's
feedparser) - mojo-captions — SRT and WebVTT subtitle/transcript parsing (no Python stdlib parallel)
- mojo-html — HTML parsing and article extraction (Python's readability)
- mojo-markdown —
CommonMark markdown parsing (Python's
markdown) - mojo-unicodedata —
Unicode normalization and case folding (Python's
unicodedata) - mojo-diff — text diffing
(Python's
difflib) - mojo-template — a
Jinja-flavored template engine (Python's
jinja2) - mojo-tar — tar archive
reading and writing (Python's
tarfile) - mojo-redis — a Redis
client (Python's
redis-py)
Issues and PRs welcome, especially real-world URLs that parse differently
than urllib.parse (attach the URL) and urljoin edge cases outside the
RFC 3986 table. Run pixi run test before sending a PR.
Built by Conor Bronsdon — host of Chain of Thought, a podcast about AI agents, infrastructure, and engineering. This library exists because every other tool in this suite eventually needs to parse a feed link, rewrite a query parameter, or resolve a relative URL. Find me on X or LinkedIn.
This is an independent personal project, not affiliated with, sponsored by, or endorsed by any company. All views expressed are my own.
Licensed under the MIT License.

