mrkeyoor.com_
Fri 07 Aug 20:54 UTC
PyPIUtilsupdated 07 Aug 2026

tldextract

tldextract splits a hostname into subdomain, domain, and public suffix using the Public Suffix List, the same community-maintained file browsers use to decide cookie scope. Splitting on dots does not work: for forums.bbc.co.uk the suffix is co.uk and the domain is bbc, but a naive rsplit gives you co. The PSL knows that co.uk, org.kg, and thousands of other multi-label suffixes are places where anyone can register, so tldextract can tell you the actual registrable domain. It returns a small dataclass with subdomain, domain, suffix, and is_private fields plus derived properties like fqdn and top_domain_under_public_suffix. On first use it downloads the current list and caches it on disk, which is both the reason it stays accurate and the reason it surprises people in production.

Verdict

The most accurate way to find the registrable domain in Python, and the right default for anything that groups or scopes by site. Configure the cache and disable the live fetch before it ships, or the first request in a cold container will be the one that pages you.

API stability4/5The extract() call and the subdomain, domain, suffix fields have been stable for years, but 5.x renamed the useful properties: registered_domain is deprecated in 5.3.1 for top_domain_under_public_suffix and is documented as removed in 6.0.0.
Docs4/5The README is task-shaped, with how-to sections for disabling fetching, custom caches, private domains, and local suffix lists, plus docstrings full of doctests; the absence of a fetch timeout by default is not called out anywhere prominent.
Maintenance5/5Pushed August 2026 with commits that week fixing real bugs like stray dots in reverse_domain_name, 19 open issues (22 counting PRs), and steady releases through 5.3.1 in December 2025.
Ecosystem4/5Around 9M weekly downloads and 2k stars, and it is the common answer for registrable-domain extraction in scraping, security, and analytics code; it stays a leaf utility with a CLI rather than a platform.

Use it if

  • You group, deduplicate, or rate limit by site and need bbc.co.uk and forums.bbc.co.uk to resolve to the same owner while bbc.co.uk and example.co.uk stay separate
  • You are writing security or anti-abuse logic that mirrors browser cookie scoping; the PSL is the same list browsers consult, so your boundary matches theirs
  • You need to distinguish registry suffixes from private ones: with include_psl_private_domains=True, waiterrant.blogspot.com has suffix blogspot.com, which is what you want when each blog is a separate tenant
  • You want the suffix list to stay current without a code change; the disk cache can be refreshed with tldextract --update or by pointing at your own mirror
Skip it if

Setup reality

pip install tldextract pulls requests, requests-file, filelock, and idna, and needs Python 3.10 or newer, so old runtimes are out. The first call is the one that bites: with a cold cache it fetches the Public Suffix List from publicsuffix.org, falls back to a GitHub mirror, writes the parsed result under $HOME/.cache/python-tldextract (override with the TLDEXTRACT_CACHE environment variable or cache_dir), and holds a file lock while doing it. There is no fetch timeout by default. If the download fails, fallback_to_snapshot=True quietly uses the list bundled at release time, so you get an answer that may be months out of date with no error. For servers, construct one TLDExtract(suffix_list_urls=()) at import time and reuse it: building a new instance re-parses the whole list into a trie on its next call, which is tens of milliseconds you do not want per request.

Patterns

Split a URL into subdomain, domain, and suffixextract-basic

import tldextract

print(tldextract.extract("http://forums.news.cnn.com/"))
# ExtractResult(subdomain='forums.news', domain='cnn', suffix='com', is_private=False)

print(tldextract.extract("http://forums.bbc.co.uk/"))
# ExtractResult(subdomain='forums', domain='bbc', suffix='co.uk', is_private=False)

The module-level extract() uses one shared extractor, so the suffix list is parsed once per process. It accepts bare hostnames as well as full URLs, and ignores the scheme, port, path, and query.

Get the domain someone actually registeredregistrable-domain

ext = tldextract.extract("http://forums.bbc.co.uk/path")

print(ext.top_domain_under_public_suffix)   # 'bbc.co.uk'
print(ext.fqdn)                             # 'forums.bbc.co.uk'
print(ext.registered_domain)                # same value, DeprecationWarning

registered_domain still works but warns and is documented as removed in 6.0.0; migrate to top_domain_under_public_suffix. Both return an empty string when there is no suffix, so localhost and IP addresses give '' rather than raising.

Stop it from downloading the list at runtimedisable-network-fetch

import tldextract

extract = tldextract.TLDExtract(suffix_list_urls=())
extract("http://www.google.com")
# uses the snapshot bundled with the installed version, no HTTP at all

This is the setting most production services want. Without it, the first call in a cold container fetches publicsuffix.org with no timeout by default, and a slow or blocked network turns a parse into a hang.

Build the extractor once and share itreuse-one-extractor

# module scope, imported everywhere
EXTRACT = tldextract.TLDExtract(suffix_list_urls=())

def site_of(url: str) -> str:
    return EXTRACT(url).top_domain_under_public_suffix

Each TLDExtract instance builds its own trie from the suffix list on first use, which costs tens of milliseconds. Creating one inside a request handler pays that on every call for no benefit.

Point the cache somewhere writablecustom-cache-location

import tldextract

extract = tldextract.TLDExtract(cache_dir="/var/cache/tldextract")

# or from the environment, no code change:
#   export TLDEXTRACT_CACHE=/var/cache/tldextract
#   export TLDEXTRACT_CACHE_TIMEOUT=2.0

The default is $HOME/.cache/python-tldextract, which does not exist for a user with no home directory and fails on a read-only root filesystem. TLDEXTRACT_CACHE_TIMEOUT is the only way to bound the fetch without touching code.

Treat blogspot.com and github.io as suffixesprivate-domains

import tldextract

extract = tldextract.TLDExtract(include_psl_private_domains=True)
print(extract("waiterrant.blogspot.com"))
# ExtractResult(subdomain='', domain='waiterrant', suffix='blogspot.com', is_private=True)

print(tldextract.extract("waiterrant.blogspot.com"))
# ExtractResult(subdomain='waiterrant', domain='blogspot', suffix='com', is_private=False)

Default is off, so every blog on blogspot.com looks like one site. Turn it on for per-tenant isolation, and use registry_suffix or top_domain_under_registry_suffix when you still need the part someone paid a registrar for.

Refresh the cached list on purposeupdate-suffix-list

# command line
#   tldextract --update
#   rm -rf $HOME/.cache/python-tldextract

import tldextract
extract = tldextract.TLDExtract()
extract.update(fetch_now=True)

The cache never expires on its own, so a long-running container keeps whatever list it fetched at startup. Run the update as a deploy step or a cron job rather than hoping the process restarts often enough.

Skip re-parsing a URL you already splitextract-from-parsed-url

from urllib.parse import urlsplit
import tldextract

EXTRACT = tldextract.TLDExtract(suffix_list_urls=())
parts = urlsplit("https://a.b.example.com/path?q=1")
print(EXTRACT.extract_urllib(parts))
# ExtractResult(subdomain='a.b', domain='example', suffix='com', is_private=False)

extract_urllib takes the netloc straight from a ParseResult or SplitResult and skips tldextract's own lenient URL splitting, so it is faster and it rejects nothing that urlsplit already accepted.

Deal with IPs, localhost, and junk inputhandle-non-domains

print(tldextract.extract("http://127.0.0.1/x").ipv4)     # '127.0.0.1'
print(tldextract.extract("https://[2001:db8::1]/").ipv6)  # '2001:db8::1'

res = tldextract.extract("http://localhost:8080")
print(res.suffix, res.domain)   # '' localhost

print(tldextract.extract("not a url at all").domain)
# 'not a url at all'

An empty suffix is the signal that there is no registrable domain. tldextract never raises on bad input, so check suffix before you trust domain, or you will happily store a chunk of free text as a site name.

Lowercase results before comparing themnormalize-case

res = tldextract.extract("HTTP://WWW.EXAMPLE.COM")
print(res)
# ExtractResult(subdomain='WWW', domain='EXAMPLE', suffix='COM', is_private=False)

site = res.top_domain_under_public_suffix.lower()   # 'example.com'

Suffix matching is case-insensitive but the returned strings keep the input's case. If you use the result as a dictionary key, a database value, or a cache key, normalize it or the same site shows up twice.

Add your own suffixes or use a local listcustom-suffix-list

import tldextract

corp = tldextract.TLDExtract(extra_suffixes=["internal", "corp.example"])
print(corp("http://api.svc.internal").suffix)   # 'internal'

vendored = tldextract.TLDExtract(
    suffix_list_urls=["file:///opt/psl/public_suffix_list.dat"],
    cache_dir="/var/cache/tldextract",
    fallback_to_snapshot=False,
)

extra_suffixes adds to the PSL for internal TLDs that will never be in it. fallback_to_snapshot=False makes a missing or unreadable list an error instead of silently downgrading you to the bundled copy.

Check a hostname from the shellcli-usage

$ tldextract http://forums.bbc.co.uk
forums bbc co.uk

$ tldextract --json waiterrant.blogspot.com
$ tldextract --private_domains waiterrant.blogspot.com
$ tldextract --update

The CLI reads the same cache as the library, so tldextract --update from a shell refreshes what your service will load next restart. --json is the form to pipe into jq when you are checking a list of hostnames.

Alternatives

PackageRegistryPick it when
publicsuffixlistPyPIYou want PSL lookups with no third-party dependencies and no runtime download; the list ships in the package and updates arrive as releases.
tldPyPIYou prefer an API built around getting the TLD or registered domain with optional URL validation and failure modes you choose.
idnaPyPIYour real problem is internationalized domain names: encoding, decoding, and validating punycode rather than splitting off the suffix.