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.parts("url").alias("d")).unnest("d")
┌─────────────────────────────────────────┬────────────────┬────────────┬───────┐
│ url                                     ┆ full_domain    ┆ sld        ┆ tld   │
╞═════════════════════════════════════════╪════════════════╪════════════╪═══════╡
│ https://www.bbc.co.uk/news/technology   ┆ bbc.co.uk      ┆ bbc        ┆ co.uk │
│ github.com                              ┆ github.com     ┆ github     ┆ com   │
│ https://blog.cloudflare.com:443/page/2/ ┆ cloudflare.com ┆ cloudflare ┆ com   │
│ 127.0.0.1                               ┆ null           ┆ 127.0.0.1  ┆ 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

parts — nulls for what isn't there

tld.parts("url")  # struct: full_domain, sld, tld
tld.registrable_domain("url")  # just "sld.tld", in one pass

Absent parts are null, and full_domain is populated only when both halves exist. This is usually what you want 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.

extract — exactly what tldextract returns

tld.extract("url")  # struct: subdomain, domain, suffix, is_private
tld.subdomain("url")
tld.top_domain("url")  # tldextract's `domain` field: the registrable label
tld.suffix("url")

Here absent parts are empty strings, faithfully reproducing tldextract.ExtractResult. Reach for this when you are porting existing tldextract code and want the behavior unchanged.

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')         nulls for absent parts

tld.extract_scalar_full("https://www.bbc.co.uk/news")
# ('www', 'bbc', 'co.uk', False)        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 78k rows/s
polars_tldextract, parallel=False 2.06M rows/s 26.5×
polars_tldextract, parallel=True 19.6M rows/s 250.7×

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 — 250× reflects 32 threads, and a 4-core laptop will land far below it. 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.1.1.tar.gz (228.0 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.1.1-cp310-abi3-win_arm64.whl (4.6 MB view details)

Uploaded CPython 3.10+Windows ARM64

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

Uploaded CPython 3.10+Windows x86-64

polars_tldextract-0.1.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.1.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.1.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.1.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.1.1-cp310-abi3-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

polars_tldextract-0.1.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.1.1.tar.gz.

File metadata

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

File hashes

Hashes for polars_tldextract-0.1.1.tar.gz
Algorithm Hash digest
SHA256 47e022c1c7582e01e0760c651003db453e7f8122c4b4365a4d0d2e4fa90c2f25
MD5 3cc48aacfca015e44c3cf4732bb6fad0
BLAKE2b-256 6a6ec4640de20f343da86c55c0b49f4d4a066032d157ddc1b2ea3d709d9798ed

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 c8567686f8b3c57010e06b1901d98ebb20284f41ca3e4e18d58a1f4d13a28f44
MD5 8fb02045f34328d428bc4dfb90b2e8fc
BLAKE2b-256 8498aa74582a71c44b6449da00d11e8124757c079c152b7228ce0375dffd091c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 bb68de46d47767b425b8e2f1f65313ba147246a397677d5d41fbabb6212c9856
MD5 e9911441c2d79f13e20eeb08913cea84
BLAKE2b-256 0d014a2d4a16cf6a09babc854f26002517d8e0b83ee78f4e1cae55756ec00cfd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 3e9ced3ee84cc7b254fbff427d82dce33f9b83ecd3991733b4aa95fbd4cc9791
MD5 f7934a293df87857deedf04a8df6f7c4
BLAKE2b-256 26438d00709e165d6b668e5ffd45a3b900a2137c001fab8b9bd2ee61c868417d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 e9080441920b37e9eea86374ebc734a8e62954485ffe9915fc567ba13a7bc96e
MD5 2af313f606d398b206edda761467e867
BLAKE2b-256 f9212a1284f1217cf3903e23c218d36d7969e5ea7a4c900795d887598bf1c4e8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ea77665f61ad81d9c0e35019bc9ad0616aefc17dba671f438106710d5d6af611
MD5 ecd2803c6239eca70016dab9017c12eb
BLAKE2b-256 d985fbbf7451723e5a45d00db4c25f40632bf245e0152ae69de39c20076fc3c0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 a734d8724f68d11d3a68a0241545805a3630cb93089e387c6f92de18c8fe9dd0
MD5 8a4cf83cd6a93093a7b6ccb9a4c3285f
BLAKE2b-256 bb602faa60a5cd7c1d2c410c5cb01c57e3f758dad56db2d5383690f51b022b74

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e624899a4ddd721c15acfa136366aafe1041972f4178c7c079569c2575b7ee03
MD5 0031c7a0d61e4524ec557e8953459297
BLAKE2b-256 93e2c8ed527cfea90ef32501ed7837f2ff212782071c33d3f5aa8eeb85345252

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for polars_tldextract-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 332035ad919c905f9ab4293277aeccce24633858237df58740a4de1fe7f2a703
MD5 3f4f5ef9d176b12b92be17bcb5e7c04c
BLAKE2b-256 2353bbd0750b93e584fa6c821e8f3da232ca0bc8388c7e99956f2325d05e5c45

See more details on using hashes here.

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