Guide¶
Everything here is runnable as-is; the # comments show the exact output.
Parsing a URL¶
Pass a URL string to Url and every part
becomes an attribute:
from action0.url import Url
url = Url("https://user:pass@www.example.com:8443/docs/api.html;v=2?q=python&page=2#intro")
print(url.scheme) # https
print(url.hostname) # www.example.com
print(url.port) # 8443
print(url.authority) # www.example.com:8443
print(url.path) # /docs/api.html
print(url.query) # q=python&page=2
print(url.fragment) # intro
print(url.username, url.password) # user pass
print(url.path_params) # v=2
Parsing and rendering round-trips:
url_str = "https://www.example.com:8443/docs/api.html?q=python#intro"
assert Url(url_str).as_str() == url_str
as_str(), str(url) and print(url) are equivalent ways to render the
URL; repr(url) looks like Url(https://…) and
never contains the password.
Constructing a URL from parts¶
All parts can be given as keyword arguments:
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
Combining a base URL string with keyword arguments replaces those parts:
url = Url("https://www.example.com/old.html?a=1", path="/new.html", query={"b": "2"})
print(url)
# https://www.example.com/new.html?b=2
Hostname and port can also be given as one authority string (handy when
that is what your config file contains):
print(Url(scheme="https", authority="example.com:8443"))
# https://example.com:8443
Changing parts¶
Attributes are plain and mutable — assign and render:
url = Url("https://www.example.com/shop/list.html?sort=price")
url.hostname = "api.example.com"
url.port = 8080
url.path = "/v2/items"
url.fragment = "top"
print(url)
# https://api.example.com:8080/v2/items?sort=price#top
url.authority = "other.org:9000" # replaces hostname and port at once
print(url)
# https://other.org:9000/v2/items?sort=price#top
Query parameters¶
url.query is a Params instance — a
MutableMapping that is aware of multiple values per name. The mapping
view works with a single value per name (the last one); multi-value access
has its own methods:
from action0.url import Url
query = Url("https://example.com/?b=2&a=1&a=3").query
print(query["b"]) # 2
print(query["a"]) # 3
print(query.get_all("a")) # ['1', '3']
print(query.get("missing", "default")) # default
print("a" in query, len(query)) # True 2
print(list(query)) # ['b', 'a']
Adding, setting and removing¶
query["b"] = 5 # replace all values (non-strings are coerced)
query.add("a", "4") # append another value
query.add("flags", [True, False]) # bools become "true" / "false"
del query["flags"]
query.remove("a", "1") # remove one value, keep the others
print(query)
# b=5&a=3&a=4
query.update({"a": "9"}, c="new") # dict.update semantics
print(query)
# b=5&a=9&c=new
Views and conversions¶
query = Url("https://example.com/?q=a&q=b&page=1").query
print(query.as_dict())
# {'q': ['a', 'b'], 'page': ['1']}
print(list(query.as_single_tuples()))
# [('q', 'a'), ('q', 'b'), ('page', '1')]
print(query.singles())
# {'q': 'b', 'page': '1'}
print(dict(query.items())) # the mapping view: one (last) value per name
# {'q': 'b', 'page': '1'}
Sorting, blank values and encoding¶
from action0.url import Params
params = Params("b=2&a=&a=1") # blank values are kept
print(params.as_str(sort=True)) # sorted output only
# a=&a=1&b=2
params.sort() # sorted persistently
print(params)
# a=&a=1&b=2
print(Params({"q": "föhn wind", "page": 1}))
# q=f%C3%B6hn+wind&page=1
Path parameters¶
The rarely seen ;key=value parameters after the path work exactly like
the query — url.path_params is a Params with a ; separator:
url = Url("https://example.com/session/cart.html;jsessionid=abc123?step=2")
print(url.path_params.as_dict())
# {'jsessionid': ['abc123']}
url.path_params["jsessionid"] = "xyz"
print(url)
# https://example.com/session/cart.html;jsessionid=xyz?step=2
Params also works standalone:
params = Params({"foo": "bar", "a": ["b", "c"]}, separator=";")
print(params)
# foo=bar;a=b;a=c
Building URLs¶
Appending path segments¶
The / operator returns a new Url with the segment appended, always
joined with exactly one slash:
api = Url("https://example.com").origin() / "api" / "v2"
print(api / "users")
# https://example.com/api/v2/users
Resolving links¶
join() resolves a (possibly relative)
reference against the URL, exactly like a browser resolves a link:
base = Url("https://example.com/docs/guide/intro.html")
print(base.join("chapter2.html")) # https://example.com/docs/guide/chapter2.html
print(base.join("../ref/index.html")) # https://example.com/docs/ref/index.html
print(base.join("/search?q=x")) # https://example.com/search?q=x
print(base.join("https://other.org/")) # https://other.org/
parent, name and suffix¶
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" # rename the last segment in place
print(url)
# https://example.com/docs/guide/outro.html?lang=en
Deriving variants¶
copy() returns an independent copy,
optionally with parts replaced:
url = Url("https://example.com:8443/index.html")
print(url.copy(scheme="http", port=None))
# http://example.com/index.html
print(url) # the original is untouched
# https://example.com:8443/index.html
Comparing and normalizing¶
Equality compares the parts; the order of query parameter names doesn’t matter, the order of multiple values per name does:
print(Url("https://example.com?a=1&b=2") == Url("https://example.com?b=2&a=1"))
# True
print(Url("https://example.com/a?x=1").origin() == Url("https://example.com/b").origin())
# True
normalize() returns an RFC 3986-normalized
copy — lowercased scheme and hostname, default ports removed, . / ..
segments resolved:
print(Url("https://example.com:443/a/../b").normalize())
# https://example.com/b
Encoding¶
Attributes hold percent-decoded (“readable”) values; rendering encodes them again. Non-ASCII hostnames become punycode:
url = Url("https://example.com", path="/reports/2026 Q1.pdf", fragment="§ 2")
print(url)
# https://example.com/reports/2026%20Q1.pdf#%C2%A7%202
parsed = Url("https://example.com/a%20b?q=c%20d")
print(parsed.path, "|", parsed.query["q"])
# /a b | c d
print(Url(scheme="https", hostname="bücher.example"))
# https://xn--bcher-kva.example
Note that a %2F inside a path segment is decoded like everything else
and therefore becomes a segment separator when rendering again.
Introspection and interop¶
url = Url("https://user:pass@example.com:8443/p;v=1?a=1&a=2#f")
print(url.is_absolute(), Url(path="/a").is_relative())
# True True
print(url.as_dict())
# {'scheme': 'https', 'username': 'user', 'password': 'pass', 'hostname': 'example.com', 'port': 8443, 'path': '/p', 'path_params': {'v': ['1']}, 'query': {'a': ['1', '2']}, 'fragment': 'f'}
print(url.as_parse_result())
# ParseResult(scheme='https', netloc='user:pass@example.com:8443', path='/p', params='v=1', query='a=1&a=2', fragment='f')
Logging and secrets¶
repr() — what debuggers, log formatters and tracebacks show — redacts
the password, while str() / as_str() keep it for real use:
url = Url("https://user:secret@example.com/")
print(repr(url))
# Url(https://user:***@example.com/)
print(str(url))
# https://user:secret@example.com/