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.

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.

Quick answer

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().

Key takeaways

  • <code>unwrap()</code> mutates the tree as it runs, so iterate over a materialised list, never a live result while modifying
  • After unwrapping you often have two adjacent text strings; <code>smooth()</code> merges them when downstream code expects one node
  • <code>replace_with()</code> is the tool when you want to keep the text but change the wrapper, not just delete it
  • Unwrapping inline tags before <code>get_text()</code> is usually pointless since get_text already ignores tag boundaries
  • The real payoff of unwrap is producing clean stored HTML, not cleaner extracted plain text
  • Unwrapping a tag that holds only whitespace can leave stray spaces that change rendered spacing

unwrap() vs decompose() vs extract()

It helps to understand the three options before choosing one, because they do very different things:

  • unwrap() removes the tag itself but leaves its children and text in place.
  • decompose() destroys the tag and everything inside it permanently.
  • extract() pulls the tag and its contents out of the tree and returns them so you can use them elsewhere.

For keeping contents while dropping the wrapper, unwrap() is the right tool.

Basic unwrap() Example

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.

Removing All Tags of One Type

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.

Unwrapping Several Tag Types at Once

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()

Watch Out for Adjacent Text Nodes

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.

A Practical Cleaning Workflow

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)

Doing This Reliably Across Many Pages

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.

Comparison snapshot

A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.

ProviderBest forProfileValue
Bright DataEnterprises needing huge pools and compliance controlsEnterprise FocusedPremium
OxylabsLarge-scale scraping and data APIsEnterprise FocusedPremium
Smartproxy (Decodo)Newcomers who want an easy dashboardBeginner FriendlyGood
SOAXPrecise city and carrier targetingAutomation FriendlyGood

The Iteration Trap: Mutating While You Loop

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.

unwrap() vs replace_with(): Keeping Text but Changing the Tag

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)

Which method fits the goal

  • unwrap: drop the wrapper, keep contents inline
  • replace_with: keep contents but under a different tag
  • wrap: add a new wrapper around existing content

When Unwrapping Actually Matters (and When It Does Not)

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.

Edge Cases: Whitespace, Self-Closing and the Root

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.

Pros and cons to weigh

Strengths

  • Removes redundant wrappers without ever touching the inner text or child tags
  • Returns the removed tag, so you can inspect or reuse it after the fact
  • Combines naturally with <code>decompose()</code> for a remove-then-flatten cleaning pass
  • Produces tidy, portable HTML suitable for storage or re-rendering in a CMS

Trade-offs

  • Mutates the tree in place, inviting iteration bugs if you loop carelessly
  • Can leave fragmented adjacent text nodes that confuse later parsing
  • Pointless overhead when the final output is plain text from get_text
  • Unwrapping whitespace-only or self-closing tags can change spacing or just delete content

Common mistakes to avoid

  • Unwrapping inside a live loop and silently skipping every other element
  • Reaching for unwrap when replace_with was the correct intent
  • Forgetting <code>smooth()</code> and then puzzling over split text strings downstream
  • Flattening formatting tags purely to extract plain text, which never needed it

Before-you-buy checklist

  • Decide whether your output is HTML or plain text before unwrapping anything
  • Materialise the search results into a list before mutating in a loop
  • Confirm you want removal, not replacement or wrapping, for each tag type
  • Plan a <code>smooth()</code> call if later code expects single, merged text nodes
  • Run <code>decompose()</code> on scripts and styles before flattening inline tags
  • Spot-check spacing around unwrapped tags that contained only whitespace
$

How to get the best value

Right-size the plan

Start on the smallest sensible tier and scale only what proves itself on your real targets.

Type before brand

Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.

Read the fine print

Check traffic limits, rotation rules and what happens on overage before you commit.

Lead with value

Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.

📖

Key terms explained

unwrap
Removes a tag while promoting its children and text into the parent in place.
replace_with
Swaps a tag for new content, useful when you want a different wrapper.
smooth
Consolidates adjacent text strings under an element into a single clean node.
In-place mutation
Editing the parsed tree directly rather than building a new copy.
Self-closing tag
An element like br or img that holds no contents to preserve when removed.

Why compare before buying?

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.

How we compare

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.

?

Frequently asked questions

What does unwrap() do in BeautifulSoup?

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.

How is unwrap() different from decompose()?

unwrap() keeps the contents and only removes the wrapper, while decompose() permanently destroys the tag along with everything inside it.

How do I remove all span tags but keep their text?

Loop over soup.find_all("span") and call tag.unwrap() on each, which strips every span while preserving the inner text.

Why do I get separate text strings after unwrapping?

Unwrapping can leave adjacent text nodes; call smooth() on the parent element to merge them into a single clean string.

Can I unwrap several different tags at once?

Yes, pass a list of tag names to find_all(), such as find_all(["span", "font", "b"]), then unwrap each result in a loop.

Does unwrap() return anything?

It returns the tag it removed, which can be useful if you want to inspect or reuse that element afterwards.

Compare on value, then decide

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.