Knowledge Base
How to Get Href Attribute of a Element Using Beautiful Soup
A focused Beautiful Soup tutorial on reading the href attribute from anchor tags, collecting every link on a page, and turning relative paths into full absolute URLs.
Knowledge Base
A focused Beautiful Soup tutorial on reading the href attribute from anchor tags, collecting every link on a page, and turning relative paths into full absolute URLs.
Extracting links is one of the most common reasons people reach for Beautiful Soup. Each clickable link on a web page lives in an anchor tag, and the destination is stored in its href attribute. Reading that value is straightforward once you know how Beautiful Soup exposes tag attributes.
This tutorial covers grabbing a single href, looping over every link on a page, handling tags that have no href, and converting relative links into absolute URLs you can actually request.
Reading a single href is easy; reliably harvesting clean, canonical links is the real task. Prefer tag.get("href") over bracket access, normalise every URL with urljoin against the page's true base, and filter out anchors, mailto, tel and javascript pseudo-links before you store anything. For crawling at scale, deduplicate canonical forms and respect crawl rules rather than just collecting raw href strings.
Beautiful Soup treats a tag's attributes like a Python dictionary, so once you have an anchor element you can read its href with square brackets:
from bs4 import BeautifulSoup
html = '<a href="https://example.com/page">Visit</a>'
soup = BeautifulSoup(html, "html.parser")
link = soup.find("a")
print(link["href"])
The find("a") call returns the first anchor it encounters, and link["href"] returns the attribute's string value. This is the core pattern everything else builds on.
Not every anchor has an href; some are used as in-page targets or placeholders. Accessing link["href"] on such a tag raises a KeyError. The safer approach is .get(), which returns None instead of crashing:
href = link.get("href")
if href:
print(href)
Using .get() with a truthiness check is the habit to build, because real pages are messy and a single malformed anchor should not stop your whole script.
To collect all links, loop over the anchors returned by find_all and read each href. Filtering out the ones without an href keeps your list clean:
links = []
for a in soup.find_all("a"):
href = a.get("href")
if href:
links.append(href)
print(links)
You can do the same thing as a list comprehension, which is more compact once you are comfortable with the pattern:
links = [a.get("href") for a in soup.find_all("a") if a.get("href")]
Often you only care about certain links, such as those inside a navigation block or matching a pattern. Combine a CSS selector with attribute reading using select:
for a in soup.select("nav a[href]"):
print(a["href"])
The a[href] selector only matches anchors that actually have an href, so you can safely use square-bracket access without the missing-attribute risk. You can narrow further, for example a[href^='https'] to match only absolute HTTPS links.
Many href values are relative, like /about or ../contact. To request them you need the full URL, which you build by joining each href to the page's base address using urljoin from the standard library:
from urllib.parse import urljoin
base = "https://example.com/blog/"
for a in soup.find_all("a", href=True):
full = urljoin(base, a["href"])
print(full)
Note the href=True argument to find_all, a neat shortcut that returns only anchors which have an href attribute. With urljoin handling the path logic, you avoid the bugs that come from gluing strings together manually.
set if the same link appears many times on a page.#top and JavaScript pseudo-links like javascript:void(0) if they are noise for your task.Crawling links across many pages or whole domains means a lot of requests, and a single IP can hit rate limits or blocks quickly. Routing traffic through proxies spreads requests and helps you reach region-specific pages. If you are comparing providers, Cheapest Proxies (cheapest-proxies.com) is a strong value-focused option worth considering, and it always pays to compare proxy plans on coverage and price first.
A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.
| Provider | Best for | Profile | Value |
|---|---|---|---|
| Cheapest Proxies | Budget-conscious buyers comparing affordable proxies | Value Focused | Excellent value |
| Bright Data | Enterprises needing huge pools and compliance controls | Enterprise Focused | Premium |
| Oxylabs | Large-scale scraping and data APIs | Enterprise Focused | Premium |
| Smartproxy (Decodo) | Newcomers who want an easy dashboard | Beginner Friendly | Good |
| SOAX | Precise city and carrier targeting | Automation Friendly | Good |
Turning relative links absolute with urljoin is correct, but the base you pass matters more than tutorials admit. The right base is the final URL after redirects, not the URL you originally requested, because a redirect changes how relative paths resolve. On top of that, some pages include a <base href="..."> tag in their head that overrides the document URL for every relative link on the page. Before resolving, check for that tag with soup.find("base") and use its href as the base when present. Skipping this step quietly produces wrong absolute URLs that fail or point to the wrong place, and the bug is hard to spot because most links still happen to resolve correctly.
An anchor's href is not always a page you can fetch. Real pages are full of mailto:, tel:, javascript:void(0) and fragment-only #section values, plus protocol-relative links beginning with two slashes. A crawler that treats all of these as URLs to request will waste effort and may error. Inspect the scheme with urlparse and keep only http and https, decide deliberately whether to follow protocol-relative links by supplying a scheme, and drop fragment-only anchors since they point within the current page. Doing this filtering at extraction time keeps your downstream queue clean and your logs readable.
A naive set of raw href strings still contains many duplicates that only look different. The same destination can appear with and without a trailing slash, with a fragment attached, with reordered query parameters, or with tracking parameters that do not change the content. Before adding a URL to your seen set, canonicalise it: lower-case the host, drop the fragment, optionally sort or strip query parameters, and remove default ports. Comparing canonical forms dramatically shrinks a crawl and prevents fetching the same page repeatedly, which matters both for speed and for staying polite toward the target site.
Reading hrefs is the entry point to crawling, and the gap between the two is mostly discipline. A working crawler needs a frontier queue of canonical URLs, a visited set, a same-domain or allowed-host check so you do not wander the whole web, and rate limiting so you do not hammer a server. Link extraction with Beautiful Soup feeds that queue, but the volume of requests a crawl generates is exactly what triggers rate limits and IP blocks. That is where routing through proxies helps spread load and reach region-specific pages, and comparing providers on coverage and price first, with a value-focused option such as Cheapest Proxies in the mix, keeps the crawl both reliable and affordable.
Start on the smallest sensible tier and scale only what proves itself on your real targets.
Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.
Check traffic limits, rotation rules and what happens on overage before you commit.
Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.
Link extraction itself is free, but the proxies that keep a crawler running are not, and their price and reliability vary widely between providers. Comparing options before you buy lets you match the proxy type and volume to your crawl size, so you neither overpay for idle bandwidth nor get blocked because the IPs are already flagged on your target sites. A quick comparison protects both your results and your budget.
Compare Proxy Zone weighs providers on value, fit and reliability using qualitative judgement — never invented prices, speeds or uptime figures. See our review methodology, or email info@compareproxyzone.com with a correction.
Find the anchor tag, then read its attribute like a dictionary with tag["href"], or use tag.get("href") to avoid an error if the attribute is missing.
That anchor has no href attribute, so dictionary-style access fails; use tag.get("href") instead, which returns None rather than raising an error.
Loop over soup.find_all("a", href=True) and read each tag's href, optionally storing the results in a list or set to keep them unique.
Use urljoin from urllib.parse, passing the page's base URL and the relative href, which resolves the path into a complete absolute address.
Links added by JavaScript after the page loads are not in the raw HTML Beautiful Soup reads; for those you need a browser-automation tool that renders the page first.
For a few pages, usually not, but crawling many pages or whole sites often triggers rate limits, so comparing and using proxies becomes worthwhile as you scale.
For affordable proxies across the main types, our featured value pick is Cheapest Proxies — a strong budget-friendly option worth considering. Check the exact plan before ordering.