Skip to main content

Vectorized, tldextract-compatible URL domain parsing for Polars.

Project description

polars-tldextract

License: MIT OR Apache-2.0 Python 3.10+

Accurate URL domain parsing for Polars, as a native Rust expression plugin.

Splitting a hostname into subdomain / domain / public suffix is not a string operation. www.bbc.co.uk and blog.cloudflare.com look identical to a regex, but the registrable domain is bbc.co.uk in one and cloudflare.com in the other — "last two labels" is wrong half the time. Getting it right requires the Public Suffix List, and in Python that means tldextract — an excellent library, but a Python function. Inside Polars it can only be driven through Expr.map_elements, one interpreter round-trip per row.

This package implements the same algorithm in Rust and exposes it as ordinary Polars expressions. It is built to produce identical output to tldextract, not merely similar output — see Correctness.

import polars as pl
import polars_tldextract as tld

df = pl.DataFrame({
    "url": [
        "https://www.bbc.co.uk/news/technology",
        "github.com",
        "https://blog.cloudflare.com:443/page/2/",
        "127.0.0.1",
        None,
    ]
})

df.with_columns(
    tld.fqdn("url").alias("fqdn"),
    tld.registrable_domain("url").alias("registrable_domain"),
    tld.suffix("url").alias("suffix"),
)
┌─────────────────────────────────────────┬─────────────────────┬────────────────────┬────────┐
│ url                                     ┆ fqdn                ┆ registrable_domain ┆ suffix │
╞═════════════════════════════════════════╪═════════════════════╪════════════════════╪════════╡
│ https://www.bbc.co.uk/news/technology   ┆ www.bbc.co.uk       ┆ bbc.co.uk          ┆ co.uk  │
│ github.com                              ┆ github.com          ┆ github.com         ┆ com    │
│ https://blog.cloudflare.com:443/page/2/ ┆ blog.cloudflare.com ┆ cloudflare.com     ┆ com    │
│ 127.0.0.1                               ┆ 127.0.0.1           ┆ null               ┆ null   │
│ null                                    ┆ null                ┆ null               ┆ null   │
└─────────────────────────────────────────┴─────────────────────┴────────────────────┴────────┘

Install

pip install polars-tldextract
# or
uv add polars-tldextract

Prebuilt wheels cover Linux (glibc and musl, x86_64 and aarch64), macOS (Intel and Apple Silicon), and Windows (x64 and arm64). There is one wheel per platform rather than one per Python version, because the extension is built against the stable ABI. An sdist is published too, so anything else builds from source given a Rust toolchain — see CONTRIBUTING.md.

Usage

Six expressions, all taking a string column of URLs or bare hostnames:

https://www.bbc.co.uk/news
tld.extract {"www", "bbc", "co.uk", false} struct: subdomain, domain, suffix, is_private
tld.fqdn www.bbc.co.uk the whole hostname
tld.registrable_domain bbc.co.uk what you register — domain.suffix
tld.subdomain www
tld.domain bbc the registrable label, tldextract's domain field
tld.suffix co.uk the public suffix

Nulls, and the one exception

The five single-value expressions return null for a part that does not exist. That matters in a DataFrame: an empty string is a value, so two rows that both failed to parse would compare equal and join to each other.

tld.extract is the exception, and deliberately so — it reproduces tldextract.ExtractResult verbatim, empty strings and all. Reach for it when porting existing tldextract code and you want the behavior unchanged; reach for anything else when the result is going into a join, a group-by, or a comparison.

fqdn vs. registrable_domain

The two differ on hosts that have no registrable domain. registrable_domain is strict — no recognized suffix means null, so an IP or a .local name drops out. fqdn just gives you the hostname:

tld.fqdn("url")  # "127.0.0.1", "localhost", "printer.local"
tld.registrable_domain("url")  # null,        null,        null

fqdn is also the normalized netloc — scheme, userinfo, port, path, query and fragment stripped, trailing root labels dropped, and the non-ASCII IDNA separators folded to . — so ftp://user:pw@ftp.gnu.org:2121/pub becomes ftp.gnu.org. Casing and punycode spelling are preserved, exactly like tldextract.

Expression namespace

Importing the package registers a .tld namespace:

df.with_columns(pl.col("url").tld.registrable_domain())
df.filter(pl.col("url").tld.suffix() == "org")

Scalars

For code that isn't holding a DataFrame — the same Rust core, no Polars round-trip:

tld.extract_scalar("https://www.bbc.co.uk/news")
# ('bbc.co.uk', 'bbc', 'co.uk')         (registrable_domain, domain, suffix), nulls for absences

tld.extract_scalar_full("https://www.bbc.co.uk/news")
# ('www', 'bbc', 'co.uk', False)        (subdomain, domain, suffix, is_private), tldextract-faithful

Private suffixes

The Public Suffix List has an ICANN section and a private section. Like tldextract, the private section is off by default:

tld.extract_scalar("pola-rs.github.io")
# ('github.io', 'github', 'io')

tld.extract_scalar("pola-rs.github.io", include_private=True)
# ('pola-rs.github.io', 'pola-rs', 'github.io')

Every expression takes the same include_private keyword.

Performance

200,000 URLs, measured with just bench:

throughput vs. map_elements
tldextract via Expr.map_elements 94k rows/s
polars_tldextract, parallel=False 2.05M rows/s 21.7×
polars_tldextract, parallel=True 21.9M rows/s 232.8×

AMD Ryzen 9 3950X (16 cores / 32 threads), 32 GB RAM, Linux 6.18 (WSL2), Python 3.12.13, Polars 1.43.

Measure a release build. just bench builds one; a plain maturin develop is unoptimized and roughly 15× slower on this workload, which measures the profile rather than the code.

Columns of 100k rows or more are split across rayon threads; pass parallel=False to force single-threaded. The threshold sits above the streaming engine's morsel size, so when Polars is already calling the plugin from several of its own worker threads each call stays single-threaded rather than nesting a fan-out inside it.

The parallel figure scales with core count — 233× reflects 32 threads, and a 4-core laptop will land far below it. It is also by far the noisiest of the three, swinging ~20% run to run with thread scheduling while the single-threaded number holds within a couple of percent. The single-threaded number is the one to reason about when Polars is already saturating your cores.

A caveat worth stating plainly: if your column has far fewer distinct URLs than rows, a dict built over Series.unique() plus replace_strict can still beat any per-row approach, including this one. This package wins on columns with high cardinality, and on code you would rather not write.

Correctness

The point of this package is not "fast domain parsing" — it is "fast domain parsing you can swap in without your results moving". tests/test_parity.py asserts (subdomain, domain, suffix) equals tldextract's answer, for both settings of include_private, over four corpora:

  1. Hand-written edge cases: schemes, userinfo, ports, IPv4, bracketed IPv6, trailing root labels, the three non-ASCII IDNA dot characters, IDN in Unicode and punycode spellings, mixed case, and degenerate input.
  2. Every rule in the Public Suffix List — each of ~9,750 rules turned into three concrete hosts, ~29,000 cases. Wildcard rules (*.ck) get a concrete label and exception rules (!www.ck) have their marker stripped so the exception path is genuinely taken. This is the check that catches divergence no hand-written suite would find.
  3. 200,000 randomly assembled URLs.
  4. A fixture set drawn from a production pipeline.

Both sides are pointed at the same list file, so a disagreement can only be an algorithm difference — never two different snapshots.

If you find an input where this package and tldextract disagree, that is a bug here. Please open an issue with the input.

The suffix list

A snapshot of the Public Suffix List is compiled into the binary, so there is no network access, no cache directory, and no first-call latency spike. tld.psl_version() reports which snapshot is in use.

To supply your own list at startup, point POLARS_TLDEXTRACT_PSL at a .dat file:

export POLARS_TLDEXTRACT_PSL=/path/to/public_suffix_list.dat

It is read once, on first use, so set it before the first extraction.

Refreshing without a restart

The list changes several times a week. A long-lived process — a notebook, a cluster, a service — would otherwise be stuck with whatever list it read when it parsed its first URL, so two functions replace it in place:

# Download the current list and load it into this process.
tld.refresh_psl()

# ...and keep a copy, so the next run need not go back to the network.
tld.refresh_psl(save_to="psl.dat")

# Or load one you already have: a path, a Path, or the list text itself.
tld.load_psl("psl.dat")

Both return the new VERSION: stamp, and take effect for every extraction that starts after they return. A query already in flight keeps the list it began with, so no single column is ever parsed against two different lists.

refresh_psl is the only function here that touches the network, and only when you call it — importing the package still does nothing. Point it at an internal mirror with tld.refresh_psl(url=...) if outbound access is restricted.

An unreadable file, an unparseable list, or one missing the ===BEGIN ICANN DOMAINS=== / ===BEGIN PRIVATE DOMAINS=== markers raises ValueError and leaves the working list untouched. The marker check matters more than it looks: a list without them parses as one undifferentiated section, and every private suffix would quietly start counting as an ICANN one — wrong output, no signal.

Compatibility

Python 3.10+ — one abi3 wheel covers all versions
Polars 1.37+ — the plugin FFI ABI is (0, 1) and unchanged across that range
Linux manylinux2014 and musllinux_1_2, x86_64 and aarch64
macOS x86_64 (10.12+) and arm64 (11.0+)
Windows x64 and arm64

If a future Polars release bumps the plugin ABI, this package fails loudly at load rather than miscomputing.

Contributing

Contributions are welcome — see CONTRIBUTING.md for the development loop, the parity requirement, and how to refresh the suffix list. docs/architecture/overview.md explains how this implementation maps onto tldextract's, which is worth reading before changing the algorithm.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Two third-party works are included or drawn upon and keep their own terms — the Public Suffix List (MPL-2.0) and the tldextract algorithm (BSD-3-Clause). See NOTICE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

polars_tldextract-0.2.1.tar.gz (282.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

polars_tldextract-0.2.1-cp310-abi3-win_arm64.whl (4.6 MB view details)

Uploaded CPython 3.10+Windows ARM64

polars_tldextract-0.2.1-cp310-abi3-win_amd64.whl (5.0 MB view details)

Uploaded CPython 3.10+Windows x86-64

polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_x86_64.whl (4.8 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ x86-64

polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_aarch64.whl (4.4 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.6 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.2 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

polars_tldextract-0.2.1-cp310-abi3-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

polars_tldextract-0.2.1-cp310-abi3-macosx_10_12_x86_64.whl (4.4 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file polars_tldextract-0.2.1.tar.gz.

File metadata

  • Download URL: polars_tldextract-0.2.1.tar.gz
  • Upload date:
  • Size: 282.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for polars_tldextract-0.2.1.tar.gz
Algorithm Hash digest
SHA256 9ed3439463b0d8b634bdb931b51064192bb09739d57ef63527ff7c703e149716
MD5 cfc4cb562e23601cec7bbe26dd399eb7
BLAKE2b-256 485e22b4bffd940490a71d438f8f8db591e1217eae9a2365e4e10a1fd00d374a

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1.tar.gz:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-win_arm64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 4b93f344ed12c89bc77d71f817c657b49a5808d3945087b775c5b1a0d9fa6bef
MD5 41fa694cd1a20e38a4519a657ede39e7
BLAKE2b-256 b7c02b5e6a04f597724feee0b4fac48360454b0412c5be13574d649701c1e69a

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-win_arm64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 025559877d3a0b9dc4bd575a915f619e08978d47b1b4ab412720833e26681759
MD5 4b61c1cba00975c18700544300d0241c
BLAKE2b-256 e828cd05e2bfa15ba4bbc37340b459d34d2abd4f12a3607959a811c882f9fe38

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-win_amd64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 fc51d05e8ba784a14edc1a98e808425086c5f7e37b4065cd011e262841c29c93
MD5 9be492be7489b54fbb2d91fa806f702c
BLAKE2b-256 313e24b0ea80d55f6345c03cbfa6aa21d45309f7d809070ce0ea31460242ed74

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_x86_64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 e22713ffa8436ce65b10fb160d9bc2cd5b0964a0446695dd63ffc58dbb95251e
MD5 66a8455c6f16c03a6742bcc4917d910d
BLAKE2b-256 5aba0bd296a52f8cc671a3d2fb6ba64057bf9d2d0424f9cf03443bbb6f57074b

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-musllinux_1_2_aarch64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 89d5da0150a866cfbf02cff4347ccf87a8993d831b2bf4f0858cab840a067465
MD5 bb87d1a3a0bbb4850306baaf4605ff34
BLAKE2b-256 13c17af141e16a8fbfa58784d69b352796928f305390ab5a638eeca7b60d81b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 f104e33e922086427d2636ff90b455bf782830ff0fa14117ec78cb972b7b5e19
MD5 6caecb76686fc52a79d7c9e8c78dc665
BLAKE2b-256 74b1bdba2576c55cb0c8e988c712747525cef604f633647f2876645f6fa52721

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 8b24860c4028f0ef6a54c53ab03bf0062c890349f3d7351241aa1989d2f16dfc
MD5 99f3d684c0382b68312af51f08c17212
BLAKE2b-256 e3d31ce70467d8e12c79655060f4ccf62adf9a260336203ae47e5421f9c9d7cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polars_tldextract-0.2.1-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for polars_tldextract-0.2.1-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 1715fde350caa466ae07b2c2712cc3826df4ac805fbe657a5f9523004a634809
MD5 039a6bc570cd243f2fe665a0ac306c14
BLAKE2b-256 578734fb1acf8471031d2fbe328687da437cecf8a38e3a00342458f38f7291b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for polars_tldextract-0.2.1-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on andrewadlof/polars-tldextract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page