Action0-Url
Easy URL parsing, manipulation and unparsing wrapper around urllib.parse's methods.
Requires Python 3.11 or newer.
Full documentation including the API reference: https://laughinjar.github.io/action0-url/
Installation
pip install action0-url # or: uv add action0-url
Usage
Either with an existing url:
from action0.url import Url
url = Url("https://www.example.com/path/filename.json?foo=bar")
url.query.set("foo", "baz")
print(url.as_str())
# https://www.example.com/path/filename.json?foo=baz
url.query.add("a", "b")
print(url.as_str())
# https://www.example.com/path/filename.json?foo=baz&a=b
url.query.add("foo", "123")
print(url.as_str())
# https://www.example.com/path/filename.json?foo=baz&a=b&foo=123
url.hostname = "action0.com"
url.port = 8443
url.path = "/public/index.html"
url.username = "user"
url.password = "pass"
url.fragment = "fragment"
# instead of url.as_str() you can also use str(url) or just print(url)
print(url)
Or construct one (and manipulate):
from action0.url import Url
url = Url(
scheme="https",
hostname="example.com",
path="index.html",
query={"a": "b", "foo": ["bar", "baz"]},
port=1234
)
print(url)
# https://example.com:1234/index.html?a=b&foo=bar&foo=baz
url.username="myuser"
url.port=None
print(url)
# 'https://myuser@example.com/index.html?a=b&foo=bar&foo=baz'
Working with query parameters
url.query is a Params instance which supports single and multiple
values per parameter name. It behaves like a dict (a MutableMapping)
where subscription works with a single value per name — the last one —
while add(), get_all() and friends handle multiple values:
from action0.url import Url
url = Url("https://example.com/?b=2&a=1&a=3")
url.query["c"] = "4" # replace/set values, like Params.set()
url.query.add("c", ["x", 5]) # non-strings are coerced, bools become "true"/"false"
url.query.remove("a", "1")
print(url.query["a"]) # the single (last) value; use get() for a default
# 3
print(url.query.get_all("c"))
# ['4', 'x', '5']
print("b" in url.query, len(url.query))
# True 3
print(url.query.as_str())
# b=2&a=3&c=4&c=x&c=5
print(url.query.as_str(sort=True))
# a=3&b=2&c=4&c=5&c=x
url.query.update({"b": 9}, token="abc") # replaces values of existing names
print(url.query.as_str())
# b=9&a=3&c=4&c=x&c=5&token=abc
url.query.sort() # persistent, unlike as_str(sort=True)
print(url.query.as_str())
# a=3&b=9&c=4&c=5&c=x&token=abc
Blank values are kept, so parsing and re-rendering is lossless:
Params("a=&b=1").as_str() is "a=&b=1" again.
Params can also be used on its own, e.g. with a ; separator as used
for path parameters:
from action0.url import Params
params = Params({"foo": "bar", "a": ["b", "c"]}, separator=";")
print(params.as_str())
# foo=bar;a=b;a=c
Building and comparing URLs
from action0.url import Url
# append path segments with "/" (always returns a new Url)
api = Url("https://example.com").origin() / "api" / "v2"
print(api / "users")
# https://example.com/api/v2/users
# resolve links like a browser does
print(Url("https://example.com/docs/intro.html").join("chapter2.html"))
# https://example.com/docs/chapter2.html
# derive variants without touching the original
url = Url("https://user:secret@example.com:8443/index.html")
print(url.copy(scheme="http", port=None))
# http://user:secret@example.com/index.html
# the authority ("hostname:port") and the origin as readable shortcuts
print(url.authority, "|", url.origin())
# example.com:8443 | https://example.com:8443
# equality compares the parts; query parameter order doesn't matter
print(Url("https://example.com?a=1&b=2") == Url("https://example.com?b=2&a=1"))
# True
# repr() never leaks the password (str() / as_str() keep it)
print(repr(url))
# Url(https://user:***@example.com:8443/index.html)
Paths, normalization and encoding
from action0.url import Url
url = Url("https://example.com/docs/guide/intro.html?lang=en")
print(url.name, "|", url.suffix, "|", url.parent)
# intro.html | .html | https://example.com/docs/guide?lang=en
url.name = "outro.html"
print(url)
# https://example.com/docs/guide/outro.html?lang=en
# RFC 3986 style normalization: casing, default ports, dot segments
print(Url("https://example.com:443/a/./b/../c").normalize())
# https://example.com/a/c
# parts are stored decoded, rendering percent-encodes them again ...
url = Url("https://example.com/a%20b", fragment="§ 2")
print(url.path, "|", url)
# /a b | https://example.com/a%20b#%C2%A7%202
# ... and non-ASCII hostnames become punycode
print(Url(scheme="https", hostname="bücher.example"))
# https://xn--bcher-kva.example
print(url.is_absolute(), Url(path="/a").is_relative())
# True True
For debugging and stdlib interoperability there are also url.as_dict()
(all parts as a plain dictionary) and url.as_parse_result() (the
urllib.parse.ParseResult named tuple).
Development
The project is managed with uv; uv run
creates and syncs the virtual environment automatically:
uv run pytest # run the tests (incl. the docstring examples as doctests)
uv run ruff check # lint
uv run ruff format # format
uv run mypy # type-check (also: uv run pyright, uv run ty check)
# build the docs (Sphinx; deployed to GitHub Pages on push to main)
uv run --group docs sphinx-build -W docs docs/_build/html
Releasing
The version lives only in src/action0/url/__init__.py (__version__).
To release: bump it, merge to main, then tag the release commit and push
the tag — the release workflow re-runs all checks, verifies the tag
matches __version__, builds sdist + wheel and publishes to PyPI via
trusted publishing:
git tag v0.1.0
git push origin v0.1.0
AI-assisted development
In the spirit of transparency: most of this project's code, tests and
documentation are written by Claude Code,
Anthropic's coding agent — under human direction and review. The designs
are specified, discussed and iterated by a human, and every change is
reviewed before it lands in main or in a release. AI-authored commits
carry a Co-Authored-By: Claude ... trailer.
About action0
This is just the namespace I like to use for my personal projects. I quite like namespaces.
Release files for action0-url 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| action0_url-0.1.0.tar.gz | 82.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| action0_url-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 96.9 kB
Release files / action0_url-0.1.0.tar.gz
| Download URL | action0_url-0.1.0.tar.gz |
|---|---|
| Size | 82.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c202e5eb7465793282d2d8342fc2128bdd2497d4cfd9dd596aef5d1d86a2c33e
|
|
BLAKE2b-256 checksum How to use checksums |
c96cc77be6980c1e677f51e99711289d02c8252afd0ca8e95adc41a8458b306d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logRelease files / action0_url-0.1.0-py3-none-any.whl
| Download URL | action0_url-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b10d76c81cc6862345b94a55c6df4e6ba1ab0c098cc1af41c07850cb83a4667d
|
|
BLAKE2b-256 checksum How to use checksums |
a5be84ebbe2caae13d91b828025cf1ddc964574f640cc4a1fd3dc5bc4eb49a34
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency log