Knowledge Base
How to Remove Tag But Keep Its Contents Using Beautifulsoup
A focused Python tutorial on unwrapping HTML tags with BeautifulSoup so the inner text and children survive while the surrounding element disappears.
Knowledge Base
A focused Python tutorial on unwrapping HTML tags with BeautifulSoup so the inner text and children survive while the surrounding element disappears.
Sometimes you do not want to delete an element and everything inside it, you just want the wrapper gone. Stripping out a redundant <span> or <font> while keeping the words it surrounds is a frequent need when cleaning scraped HTML before storage or display.
BeautifulSoup makes this easy with unwrap(). This tutorial shows how to remove a tag but keep its contents, how it differs from deletion methods, and how to apply it across many tags at once.
Call tag.unwrap() to delete an element while its children and text stay in place. It works in-place on the parsed tree and returns the removed tag. For the reverse, wrapping existing content in a new element, use wrap(); to swap a tag for different content, use replace_with().
It helps to understand the three options before choosing one, because they do very different things:
For keeping contents while dropping the wrapper, unwrap() is the right tool.
Suppose you have a link wrapped around some text and you want plain text in its place:
from bs4 import BeautifulSoup
html = '<p>Read the <a href="/full">full article</a> now.</p>'
soup = BeautifulSoup(html, "lxml")
soup.a.unwrap()
print(soup.p)
# <p>Read the full article now.</p>
The anchor tag is gone, but the words "full article" remain exactly where they were. The method also returns the tag it removed, in case you want to inspect it.
To strip every occurrence of a tag, loop over find_all() and unwrap each one. This is common when you want to flatten formatting tags like <b>, <i> or <span>:
for tag in soup.find_all("span"):
tag.unwrap()
After this runs, the text content is identical but the span wrappers no longer appear in the markup.
Pass a list to find_all() to target multiple tag names in a single pass:
for tag in soup.find_all(["span", "font", "b"]):
tag.unwrap()After unwrapping, BeautifulSoup may leave separate neighbouring text strings rather than one merged string. This rarely matters for the rendered output, but if you parse the tree afterwards it can surprise you. Calling smooth() on a parent consolidates adjacent text nodes:
soup.p.smooth()
Use it when your downstream logic depends on a single clean text node per element.
When sanitising scraped HTML, a typical sequence is: remove unwanted elements entirely with decompose(), unwrap the formatting tags you want to flatten, then read the result. For example, drop scripts and styles, then flatten inline tags:
for junk in soup.find_all(["script", "style"]):
junk.decompose()
for inline in soup.find_all(["span", "font"]):
inline.unwrap()
clean = soup.get_text(separator=" ", strip=True)Cleaning one document is trivial; cleaning thousands as part of an ongoing scrape introduces new concerns, chiefly fetching all those pages without being throttled. Proxies smooth out large collection jobs, and because price and reliability differ a lot between providers, comparing them on value first pays off. Cheapest Proxies is our featured value pick for budget-conscious projects.
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 |
Because unwrap() edits the tree as it executes, the most common bug is modifying a collection while iterating over it. If you loop over a live generator and unwrap inside the loop, you can skip elements or hit confusing state. Convert the search to a concrete list first so the iteration is fixed before any mutation begins.
for tag in list(soup.find_all("span")):
tag.unwrap()
The base article's loop happens to work because find_all already returns a list, but the habit of wrapping with list() protects you when you later switch to a generator-based search or chain filters.
Unwrapping deletes the wrapper entirely. Sometimes you instead want to demote a tag, turning a <a> into a <span>, or promote bold spans into real <strong> tags. That is a replacement, not a removal. Build the new tag, move the contents, then swap.
new = soup.new_tag("strong")
new.string = old.get_text()
old.replace_with(new)
If your end goal is plain text via get_text(), unwrapping inline formatting tags is wasted effort because get_text already flattens them. Unwrap earns its place when you are storing or re-displaying HTML: flattening redundant <font>, presentational <span> and legacy <b> tags produces cleaner markup for a CMS, a sanitiser whitelist or a diff. Know your output format before deciding whether unwrap is even needed.
Unwrapping is not always neutral. A tag wrapping only a space can merge words once removed, subtly changing readable spacing. Self-closing tags like <br> have no contents, so unwrapping them just deletes them, which may not be what you intended. And you cannot unwrap the top-level element of the tree, since it has no parent to absorb the children. When this cleaning is part of a large ongoing scrape, the bottleneck is fetching all those pages reliably rather than the unwrapping itself, so comparing proxy providers on value, including budget-focused options like Cheapest Proxies, keeps the pipeline both stable 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.
Choosing unwrap() over decompose() is a small decision that protects your data, the wrong method silently deletes content you needed. The same care applies upstream when you gather pages: comparing proxy providers on value before buying prevents both wasted spend and incomplete datasets.
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.
It removes a tag from the tree while leaving the tag's children and text in place, effectively replacing the tag with its own contents.
unwrap() keeps the contents and only removes the wrapper, while decompose() permanently destroys the tag along with everything inside it.
Loop over soup.find_all("span") and call tag.unwrap() on each, which strips every span while preserving the inner text.
Unwrapping can leave adjacent text nodes; call smooth() on the parent element to merge them into a single clean string.
Yes, pass a list of tag names to find_all(), such as find_all(["span", "font", "b"]), then unwrap each result in a loop.
It returns the tag it removed, which can be useful if you want to inspect or reuse that element afterwards.
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.