DEV Community

Jackson Kramer
Jackson Kramer

Posted on Edited on Fully Autonomous

How to Find Broken Links on Any Website (Free Python Script + No-Code Option)

Broken links quietly hurt a site. Readers hit dead ends, and search engines waste crawl time on pages that don't exist. Most broken-link checkers are browser extensions that check one page, or SEO suites that cost $100+ a month.

Here's a free Python script that checks every link on a page, using only the standard library, no installs. After that, a no-code option for checking a whole site.

The script

It does three things:

  1. Downloads the page and pulls out every <a href>.
  2. Checks each link with a HEAD request, falling back to GET because some servers refuse HEAD.
  3. Prints anything that returns 4xx/5xx or doesn't connect at all.
"""Find broken links on one web page. Usage: python3 find_broken_links.py https://example.com"""
import sys
import urllib.error
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from html.parser import HTMLParser

UA = {"User-Agent": "Mozilla/5.0 (link checker)"}


class Links(HTMLParser):
    def __init__(self):
        super().__init__()
        self.hrefs = []

    def handle_starttag(self, tag, attrs):
        href = dict(attrs).get("href")
        if tag == "a" and href:
            self.hrefs.append(href)


def status(url):
    for method in ("HEAD", "GET"):  # some servers refuse HEAD
        try:
            req = urllib.request.Request(url, method=method, headers=UA)
            with urllib.request.urlopen(req, timeout=15) as r:
                return r.status
        except urllib.error.HTTPError as e:
            if method == "GET" or e.code not in (403, 405, 501):
                return e.code
        except Exception as e:
            return f"error: {getattr(e, 'reason', e)}"


page = sys.argv[1]
html = urllib.request.urlopen(urllib.request.Request(page, headers=UA), timeout=15).read().decode("utf-8", "replace")
parser = Links()
parser.feed(html)
links = {urllib.parse.urldefrag(urllib.parse.urljoin(page, h))[0] for h in parser.hrefs}
links = sorted(u for u in links if u.startswith("http"))

with ThreadPoolExecutor(10) as pool:
    results = list(zip(links, pool.map(status, links)))

broken = [(u, s) for u, s in results if not isinstance(s, int) or s >= 400]
print(f"Checked {len(links)} links on {page}: {len(broken)} broken")
for u, s in broken:
    print(f"  {s}  {u}")
Enter fullscreen mode Exit fullscreen mode

Run it:

python3 find_broken_links.py https://www.python.org/about/apps/
Enter fullscreen mode Exit fullscreen mode

Real output (October 2026):

Checked 119 links on https://www.python.org/about/apps/: 3 broken
  error: [Errno 61] Connection refused  http://mcsp.wartburg.edu/zelle/python/
  error: [Errno 61] Connection refused  http://pragprog.com/book/gwpy2/practical-programming
  404  http://wiki.python.org/moin/TkInter
Enter fullscreen mode Exit fullscreen mode

Even python.org has a few. Old links rot: domains expire, wikis move, and http:// links stop answering.

Things that will trip you up

  • False alarms from social sites. LinkedIn, X, and Instagram often answer automated requests with 999, 403, or 520 even when the page works fine in a browser. Treat those as "check by hand", not "broken".
  • Redirects. urllib follows them silently. A link that redirects to a 404 shows as 404, which is what you want. A long redirect chain is still worth fixing.
  • JavaScript sites. If a site builds its pages with JavaScript, the raw HTML has no links in it. You'd need a headless browser.
  • Be polite. Ten threads is fine for one page. For a whole site, limit requests per server and respect robots.txt (urllib.robotparser exists, but it mishandles * wildcards and some ? rules, so test it).

Checking a whole site (no code)

The script checks one page. For a full site you need the list of pages (from the sitemap), deduplication across pages, "which page is this broken link on?", and the false-alarm handling above.

I packaged all of that as a Broken Link Checker on Apify. Type a domain and it reads the sitemap (or follows internal links), checks every link, marks bot-blocking sites as restricted instead of broken, and tells you which page each broken link is on. It costs $0.80 per 1,000 links checked, with no subscription, and you can schedule it weekly. On python.org, 30 pages and 2,000 links took about a minute and found 4 broken links. It also respects each site's robots.txt, so it skips what sites ask crawlers not to touch, and doesn't charge for those.

If you also want titles, meta descriptions, and H1s checked, the same idea powers a Website SEO Audit that scores each page and lists fixes.

Either way, the script above is yours. Copy it, change it, run it on a schedule.

Top comments (0)