url-normalize
Normalize URL spelling consistently for storage, comparison, and display. Use url_normalize() for normalized URLs and url_humanize() for readable display URLs.
Requires Python 3.11 or newer. Normalization operates on URL text; it does not fetch pages or follow redirects.
Installation
Install the library and CLI:
python -m pip install url-normalize
Or install the CLI in its own environment with uv:
uv tool install url-normalize
Quick start
from url_normalize import url_normalize
print(url_normalize("HTTP://Example.COM:80/a/../b"))
# Output: http://example.com/b
print(url_normalize("bücher.de/résumé"))
# Output: https://xn--bcher-kva.de/r%C3%A9sum%C3%A9
From the terminal:
url-normalize "HTTP://Example.COM:80/a/../b"
# Output: http://example.com/b
Normalization behavior
| Behavior | Input | Output |
|---|---|---|
| Add a missing scheme | example.com |
https://example.com/ |
| Lowercase scheme and host; remove a default port | HTTP://Example.COM:80/ |
http://example.com/ |
| Resolve dot segments | https://example.com/a/../b |
https://example.com/b |
| Encode Unicode hosts and paths | https://bücher.de/résumé |
https://xn--bcher-kva.de/r%C3%A9sum%C3%A9 |
| Remove trailing host dots, including Unicode equivalents | https://example.com。/ |
https://example.com/ |
| Preserve encoded path separators; uppercase escape hex | https://example.com/a%2fb |
https://example.com/a%2Fb |
| Preserve query order and repeated keys | https://example.com/?b=2&a=1&a=3 |
https://example.com/?b=2&a=1&a=3 |
| Preserve explicit empty query values | https://example.com/?flag=&flag |
https://example.com/?flag=&flag |
In query parameters, +, %2B, and %20 remain distinct. Parameter filtering is disabled by default. Fragments are preserved and normalized; hashbang fragments (#!) are converted to the _escaped_fragment_ query parameter.
Normalization applies consistent spelling rules; it does not establish whether two URLs serve the same content. Applications using normalized URLs as persistent cache keys or identifiers should review the changelog when upgrading.
Python API
Both functions accept a URL as str or None and return str or None. An empty string and None are returned unchanged. Malformed input can raise ValueError, including UnicodeError for invalid hostnames.
The following options are keyword-only and apply to both functions:
| Option | Default | Purpose |
|---|---|---|
default_scheme |
"https" |
Scheme to add when the URL has none. |
default_domain |
None |
Domain to add to an absolute path such as /images/logo.png. |
filter_params |
False |
Retain only allowlisted query parameters. |
param_allowlist |
None |
Override built-in rules with a list of names or a dictionary mapping domains to lists. Requires filter_params=True. |
Unicode URL characters always use UTF-8 percent encoding.
Query parameter filtering
With filter_params=True, only explicitly allowed parameters remain. Built-in rules cover Google, Bing, Baidu, and YouTube; see the domain rules for their parameter names.
Unknown domains have an empty default allowlist, so filtering removes all their query parameters unless you supply a custom rule. A custom allowlist replaces the built-in rules.
from url_normalize import url_normalize
# Use the built-in Google rule.
result = url_normalize(
"www.google.com/search?q=test&utm_source=test", filter_params=True
)
print(result)
# Output: https://www.google.com/search?q=test
# Apply a list of allowed names to any domain.
result = url_normalize(
"example.com/?page=1&id=123&ref=test",
filter_params=True,
param_allowlist=["page", "id"],
)
print(result)
# Output: https://example.com/?page=1&id=123
# Use domain-specific rules, including Unicode parameter names.
result = url_normalize(
"https://example.com/?r%C3%A9sum%C3%A9=1&drop=2",
filter_params=True,
param_allowlist={"example.com": ["résumé"]},
)
print(result)
# Output: https://example.com/?r%C3%A9sum%C3%A9=1
Allowed names use the same normalization as query keys, including Unicode and spaces. Domain rules match normalized hostnames and ignore a leading www. and the port. Rules do not automatically apply to other subdomains.
Default domain and scheme
Use default_domain for paths starting with a single /. URLs starting with // already specify their host. This option does not resolve relative paths such as ../images/logo.png against a page URL.
from url_normalize import url_normalize
result = url_normalize(
"/images/logo.png", default_domain="example.com", default_scheme="http"
)
print(result)
# Output: http://example.com/images/logo.png
Human-readable URLs
url_humanize() normalizes its input, then decodes components only when the display URL normalizes back to the same result. Some escapes therefore remain encoded.
from url_normalize import url_humanize, url_normalize
normalized = "https://xn--bcher-kva.de/r%C3%A9sum%C3%A9"
readable = url_humanize(normalized)
print(readable)
# Output: https://bücher.de/résumé
assert url_normalize(readable) == normalized
Command-line usage
The CLI accepts one URL per invocation and prints the result to standard output. Quote URLs containing shell characters such as &.
# Keep selected query parameters.
url-normalize -f -p page,id "example.com/?page=1&id=123&ref=test"
# Output: https://example.com/?page=1&id=123
# Supply a domain and scheme for an absolute path.
url-normalize -d example.com -s http "/images/logo.png"
# Output: http://example.com/images/logo.png
# Display a readable URL.
url-normalize -H "https://xn--bcher-kva.de/r%C3%A9sum%C3%A9"
# Output: https://bücher.de/résumé
Use url-normalize --help for all options and url-normalize --version for the installed version. Normalization errors are written to standard error with exit status 1; invalid command-line arguments use exit status 2.
To run without a persistent tool installation:
uvx url-normalize "example.com"
# Output: https://example.com/
Contributing
After cloning the repository, install the development dependencies and run the checks with uv:
uv sync --locked --extra dev
uv run --locked --extra dev python -m pytest
uv run --locked --extra dev pre-commit run --all-files
The test suite requires 100% statement coverage. CI tests Python 3.11 through 3.15 on Linux, macOS, and Windows. Please include regression tests with bug fixes.
Changelog and license
See CHANGELOG.md for release history and upgrade notes. Distributed under the MIT License.
Inspired by Sam Ruby's urlnorm.py.
Metadata
Release files for url-normalize 3.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 | Provenance | Actions |
|---|---|---|---|---|
| url_normalize-3.1.0.tar.gz | 30.5 kB | Without provenance | Details for url_normalize-3.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| url_normalize-3.1.0-py3-none-any.whl | Python 3 | none | any | Details for url_normalize-3.1.0-py3-none-any.whl |
Total release size: 49.2 kB
Release files / url_normalize-3.1.0.tar.gz
| Download URL | url_normalize-3.1.0.tar.gz |
|---|---|
| Size | 30.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
138912833c0005ec77e756ec220590f8971ff046227280790ceb00f5ab9d56b6
|
|
BLAKE2b-256 checksum How to use checksums |
84d3af36d36d189cb33237732a7fe6bba2df08189ac8c5975dab0510d5c6a5f2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / url_normalize-3.1.0-py3-none-any.whl
| Download URL | url_normalize-3.1.0-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
34c386c6b2dbde760eaaaa8542dc29995c75f2baf94acceee18e5b6b29579469
|
|
BLAKE2b-256 checksum How to use checksums |
7d3dae9ba568240002f25d676df745208aec077767cd63e68cbb93bd2650c4ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for url-normalize 3.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.