Introduction

Welcome back to Extracting Data with Capture Groups in Python! You've now completed the first lesson of this course, and you're building impressive skills in regex-based data extraction. In the previous lesson, you mastered named capture groups: a feature that lets you extract structured data with meaningful names like year, month, and day instead of cryptic numeric indices. You built a date parser that returns clean dictionaries and handles invalid input gracefully. This was a major step forward, transforming regex from a pattern matcher into a data extraction tool.

Today, we're exploring another powerful feature that works hand in hand with capture groups: backreferences. While named groups let you extract and retrieve matched text, backreferences let you reuse that captured text within the same pattern. This opens up entirely new pattern-matching capabilities. You'll be able to find repeated words in text, match paired delimiters like HTML tags, and enforce consistency across different parts of a pattern. These skills are essential for tasks like detecting duplicated text, parsing structured formats, and validating patterns with internal consistency requirements.

This lesson will show you two compelling applications of backreferences. First, we'll find consecutive repeated words in text, catching common writing errors where words like "the the" appear accidentally. Second, we'll extract content from matched HTML-style tags, ensuring that opening and closing tags correspond correctly. Both examples demonstrate how backreferences enforce relationships between different parts of a pattern, something impossible with the regex tools you've learned so far. Let's begin by understanding the problem that backreferences solve.

The Challenge of Pattern Consistency

Consider two common text-processing scenarios that initially seem straightforward but reveal a subtle complexity. First, imagine you want to find words that appear twice in a row: "the the," "is is," or "no no." Your instinct might be to use a pattern like \b\w+\s+\w+\b, which matches two consecutive words. But this pattern matches any two words, not specifically repeated words. It would match "the cat" just as readily as "the the," giving you far too many false positives.

Second, suppose you're extracting content from HTML-like tags: <title>Introduction</title>. You know how to match the opening tag <\w+>, the content [^<]+, and the closing tag <\/\w+>. But this pattern has a critical flaw: it would happily match <title>Introduction</span>, accepting mismatched tags. In real-world parsing, such mismatches indicate corrupted data that should be rejected, not extracted.

Both scenarios share a fundamental requirement: one part of the pattern must match the exact same text as another part. We need a way to say, "match this word, then match that same word again," or "match this tag name, then match that same tag name in the closing tag." This is precisely what backreferences enable. They let you refer back to text captured by an earlier group, enforcing consistency within a single pattern.

Understanding Backreferences

A backreference is a special construct that refers to text already captured by a capture group earlier in the same pattern. When the regex engine processes a backreference, it doesn't match a new pattern; instead, it matches the exact literal text that was captured by the referenced group. This creates a powerful constraint: different parts of your pattern can be forced to contain identical content.

The syntax for backreferences is straightforward: use a backslash followed by the group number. The first capture group is referenced as \1, the second as \2, and so on. If your pattern contains (\w+)\s+\1, the engine first captures one or more word characters into group 1, then matches whitespace, then matches \1, which requires the exact same text that group 1 captured. If group 1 captured "hello," then \1 will only match "hello" again.

It's important to understand what makes backreferences different from repeating a pattern. The pattern (\w+)\s+(\w+) captures two words, but they can be completely different. The pattern (\w+)\s+\1 also captures one word (in group 1) and matches another word, but that second word must be identical to the first. This distinction is subtle but profound: backreferences enforce textual identity, not just pattern similarity.

Basic Backreference Syntax

Let's examine the syntax more closely with a concrete example. When you write \b(\w+)\s+\1\b, you're creating a pattern with three key components. The first component, \b(\w+), uses a word boundary followed by a capture group containing one or more word characters. This captures a complete word. The second component, \s+, matches one or more whitespace characters, allowing for spaces, tabs, or newlines between words. The third component, \1\b, is where backreferences shine: \1 references the text captured by group 1, and \b ensures we match a complete word boundary.

Notice how we only need one set of parentheses to create a capture group. The \1 isn't a pattern definition; it's a reference to what was already captured. If the first (\w+) matches "test," then \1 will only match "test" again, not any other word. This pattern structure will form the foundation of our repeated word finder.

Finding Repeated Words

Now we'll implement a function that finds all consecutively repeated words in a text. This is useful for proofreading, catching common typing errors, and analyzing text quality. The function uses backreferences to ensure we only capture truly duplicated words, not just any two consecutive words.

The find_repeated_words function applies our backreference pattern to the input text. The re.findall() function returns a list of all matches, but here's an important detail: when you use findall() with a pattern containing groups, it returns only the captured group content, not the entire match. So even though our pattern matches "is is" in the text, findall() returns just "is" (the content of group 1). This is actually convenient: we get the repeated word itself, not the word plus its duplicate.

The output reveals something interesting. We found "is is" and returned "is." We found "No No" and returned "No." We also found "no no" and returned "no." But wait: the text contains "No No no no," which appears to have four consecutive occurrences of the same word. Why did we only get two matches instead of three? This happens because regex matches are non-overlapping. After matching "No No," the engine continues from after that match, where it finds "no no" as a separate match. The fact that "No" and "no" differ in case means they're treated as distinct by the default case-sensitive matching.

How the Backreference Pattern Works

Let's trace through exactly how the regex engine processes our pattern against the text "This is is a test." Understanding the matching process will help you build more complex backreference patterns with confidence.

The engine starts at the beginning: "This." The \b(\w+) matches "This" and captures it in group 1. Then \s+ matches the space. Now \1 looks for "This" again, but finds "is" instead. No match. The engine moves forward and tries again at "is." Now (\w+) captures "is" in group 1, \s+ matches the space, and \1 checks whether the next word is also "is." It is! The pattern matches completely, and "is" (the content of group 1) is added to the results.

The key insight is that backreferences are dynamic: \1 doesn't mean "match the word 'is'." It means "match whatever text group 1 captured in this particular attempt." Each time the engine tries the pattern at a new position, group 1 might capture different text, and \1 adapts accordingly. This dynamic binding is what makes backreferences so powerful: they create relationships between pattern parts without hardcoding specific values.

Case Sensitivity in Backreferences

An important characteristic of backreferences is that they match exact text, including case. When group 1 captures "No" (with an uppercase N), the backreference \1 will only match "No" with the exact same case, not "no" or "NO." This is why our output contains both "No" and "no" as separate matches: they're different words from the regex engine's perspective.

If you need case-insensitive matching, you can use regex flags to require this behaviour. We won't delve into this topic yet, as we'll be exploring regex flags as part of the next course. In any case, for proofreading applications, case-sensitive matching is often preferable because it catches legitimate repetitions while preserving the distinction between words that differ only in capitalization.

Matching Paired Delimiters

Now let's explore a more sophisticated application of backreferences: matching content enclosed in paired delimiters. Many text formats use paired tags or brackets: HTML tags like <title>...</title>, BBCode like [b]...[/b], or even custom markup. The challenge is ensuring that the opening and closing delimiters match correctly. Backreferences provide an elegant solution.

Consider HTML-style tags. An opening tag looks like <tagname>, and a closing tag looks like </tagname>. We want to extract both the tag name and the enclosed content, but only when the tags match. The pattern needs to capture the tag name from the opening tag, then use a backreference in the closing tag to ensure they're identical. This prevents mismatched tags like <title>Introduction</span> from being accepted.

Building the Tag Matching Pattern

Let's implement a function that extracts content from matching HTML-style tags. The pattern combines capture groups for both the tag name and content with a backreference to enforce tag consistency.

This pattern has three major parts. First, <(\w+)> matches an opening tag and captures the tag name in group 1. The tag name must consist of word characters (letters, digits, underscores). Second, ([^<]+) captures the content between tags in group 2. The negated character class [^<] matches any character except <, and the + quantifier ensures we match at least one character. This approach greedily consumes content but stops immediately when it encounters <, preventing the pattern from consuming too much text. Third, <\/\1> matches a closing tag where \1 references the tag name captured in group 1. The forward slash is escaped as \/ because we're matching the literal characters </.

The function uses re.findall(), which returns a list of tuples when the pattern contains multiple groups. Each tuple contains the tag name (group 1) and the content (group 2). We then transform these tuples into a more readable list using a list comprehension. This gives us structured data showing exactly what tags were found and what content they contained.

Understanding the Tag Extraction Pattern

The backreference \1 in the closing tag is the critical element that enforces correctness. When the engine matches <title>, group 1 captures "title." Later, when processing the closing tag, \1 requires "title" again. If the text contains <title>Introduction</span>, the pattern fails: group 1 captured "title," but the closing tag contains "span," which doesn't match \1. The pattern only succeeds when opening and closing tags contain identical text.

The negated character class [^<]+ deserves attention. Why not use a simpler pattern like .+? The dot matches any character, so .+ would seem sufficient. However, the dot combined with + is greedy: it matches as many characters as possible. In the text <title>Introduction</title> and <span>Quick Start</span>, the pattern <(\w+)>.+<\/\1> would match from the first <title> all the way to the final </title> at the end, consuming everything in between, including the span tags. This is called catastrophic greediness. By using [^<]+ instead, we explicitly stop at the first < character, ensuring we extract only the content between the immediately adjacent tags.

Viewing the Tag Extraction Results

Let's test our tag extraction function with a string containing multiple HTML-style tags. This will demonstrate how the pattern handles multiple matches and correctly pairs opening and closing tags.

The test string contains three distinct tagged sections: title, span, and paragraph tags. Each section has properly matched opening and closing tags. The pattern should identify all three sections and extract both the tag names and their content. Notice that the text between different tag pairs (words like "and" and "then") is not captured: our pattern specifically targets tagged content, ignoring everything else.

The output confirms successful extraction. Each tuple in the list contains a tag name and its corresponding content. The first tuple, ('title', 'Introduction'), shows the title tag contained "Introduction." The second tuple extracted "Quick Start" from the span tag, and the third extracted "Details" from the paragraph tag. All three sections were correctly identified, and the backreferences ensured we never mistakenly paired mismatched tags.

Why This Avoids Lazy Quantifiers

You might recall from previous courses that lazy quantifiers (using ? after a quantifier) make patterns match as little as possible. For example, .+? matches one or more characters but stops as soon as the rest of the pattern can match. We could theoretically write our tag pattern as <(\w+)>(.+?)<\/\1> using a lazy quantifier. This would work for simple cases, but it has subtle problems.

The negated character class approach [^<]+ is more explicit and reliable. It clearly states, "match anything except <," making the pattern's behavior obvious to anyone reading the code. It also performs better: the regex engine doesn't need to backtrack and try different amounts of text; it simply stops at the first <. With a lazy quantifier like .+?, the engine tries matching one character, then checks if the rest of the pattern succeeds; if not, it tries two characters, and so on. This repeated backtracking is less efficient than [^<]+, which moves forward decisively and stops at the boundary character.

Conclusion and Next Steps

Excellent work! You've now mastered backreferences, a powerful tool that extends regex beyond simple pattern matching to enforce internal consistency. In this lesson, you learned how backreferences let you reuse captured text within the same pattern using the \1, \2 syntax. You discovered two compelling applications: finding consecutively repeated words with \b(\w+)\s+\1\b and extracting content from matched HTML-style tags with <(\w+)>([^<]+)<\/\1>. Both patterns demonstrate how backreferences create relationships between different pattern components, ensuring that text in one location matches text in another.

The key insight is that backreferences enforce textual identity, not pattern similarity. When you write (\w+)\s+\1, the second word must be exactly the same as the first, character for character. This is fundamentally different from writing (\w+)\s+(\w+), which captures two words that can be completely different. You also learned an important technique for controlling greediness: using negated character classes like [^<]+ instead of lazy quantifiers, giving you more precise control over where matching stops.

These backreference patterns form essential building blocks for text processing tasks. Whether you're parsing log files, extracting structured data, validating input formats, or detecting text errors, backreferences help you express complex matching requirements that would be impossible with simpler regex tools. Combined with the named groups you learned previously, you now have a comprehensive toolkit for extracting and validating structured text data.

In the next lesson, we'll shift focus to practical extraction patterns for common data types like emails, URLs, and prices. You'll learn how to combine character classes, quantifiers, anchors, and capture groups to build robust extractors for real-world data. But first, let's cement your understanding of backreferences through hands-on practice. The upcoming exercises will challenge you to fix common backreference mistakes, adapt patterns to new contexts like BBCode, find symmetrical phrasing in text, and make HTML extractors more flexible. Get ready to put these powerful patterns to work!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal