Positive Look-Behind in Regular Expressions

Regular Expressions Mar 20, 2026

Positive Look-Behind in Regular Expressions

Positive look-behind (?<=...) is a zero-width assertion that checks whether a pattern is preceded by another pattern without consuming those characters. It lets you match something only when it is immediately preceded by a specific context. In data science this is extremely useful for extracting numbers that come after “Price: ”, product codes that follow “SKU:”, or IDs that appear after a known label — all without including the preceding text in the final match.

TL;DR — Positive Look-Behind

  • (?<=...) → assert that ... must precede the match
  • Zero-width: the look-behind text is not part of the captured result
  • Perfect for context-sensitive extraction from logs and reports
  • Works seamlessly with pandas .str.extract()

1. Basic Positive Look-Behind

import re

text = "Price: 1250 USD, Tax: 87 EUR, Total: 1337 USD"

# Numbers preceded by "Price: "
print(re.findall(r"(?<=Price: )\d+", text))      # ['1250']

# Product codes preceded by "SKU: "
print(re.findall(r"(?<=SKU: )([A-Z0-9-]+)", "SKU: ABC123 sold, SKU: XYZ789 in stock"))   # ['ABC123', 'XYZ789']

2. Real-World Data Science Examples with Pandas

import pandas as pd

# Demo rows so this example runs as-is (in your project: df = pd.read_csv("logs.csv"))
df = pd.DataFrame({"log": [
    "Price: 1,250.75 USD for ORD-12345 2026-03-19 alice@example.com verified SKU: ABC-123 in stock",
    "Tax: 87.50 EUR for ORD-98765 2026-03-20 bob@spam.ru SKU: XYZ-789 out of stock",
    "Cancelled ORD-55555 Price: 12.00 GBP carol@data.org Order ID: 777",
]})

# Example 1: Extract amounts only when preceded by "Price: " (positive look-behind)
df["price_amount"] = df["log"].str.extract(r"(?<=Price: )(\d+(?:,\d+)?(?:\.\d+)?)")

# Example 2: Extract order IDs only when preceded by "Order ID: "
df["order_id"] = df["log"].str.extract(r"(?<=Order ID: )(\d+)")

# Example 3: Extract SKUs only when preceded by "SKU: "
df["sku"] = df["log"].str.extract(r"(?<=SKU: )([A-Z0-9-]+)")

print(df[["price_amount", "order_id", "sku"]])

3. Advanced Positive Look-Behind

# Look-behind with a character class
text = "Profit: +1250 USD, Loss: -340 EUR"
print(re.findall(r"(?<=[+-])\d+", text))         # ['1250', '340']

# Python's re requires fixed-width look-behind: (?<=Price:\s*) raises re.error.
# Put the variable part outside and capture the value instead:
print(re.findall(r"Price:\s*(\d+)", "Price:   1250 USD"))   # ['1250']

4. Best Practices in 2026

  • Use positive look-behind (?<=...) whenever you need context before the match without including it
  • Combine with capturing groups to extract only the part you want
  • Keep look-behind expressions simple and fixed-width where possible for maximum compatibility and readability
  • Pre-compile patterns that contain look-behind for repeated use on large datasets
  • Use with pandas .str.extract() for vectorized zero-width assertions across entire DataFrames

Positive Look-Behind for Clean Values

A positive look-behind (?<=...) matches a position that is preceded by a given text. The prefix decides which values you want, but only the value itself is returned, so there is nothing to strip afterwards.

It works well for labelled values with a fixed prefix, such as currency codes, field names like id= or hashtags. Because Python requires a fixed-width look-behind, list each prefix with the same length, or use separate look-behinds joined by | outside them, such as (?:(?<=\$)|(?<=USD ))\d+.

Joining look-behinds this way is allowed because each one is fixed-width on its own.

Worked Example: Amounts with Two Different Prefixes

We extract amounts written either as $25 or as USD 25 from a column of notes.

import pandas as pd

notes = pd.Series(["Paid $25 today", "Refund USD 40", "Fee EUR 5", "Tip $3 and USD 7"])
pattern = r"(?:(?<=\$)|(?<=USD ))\d+"

amounts = notes.str.findall(pattern)
print(amounts.tolist())
print(amounts.explode().dropna().astype(int).sum())

The results are [['25'], ['40'], [], ['3', '7']] and the total is 75. The euro fee is skipped because neither prefix matches, and the two look-behinds of different widths work because each is fixed on its own.

Practical Tips

Escape special characters in prefixes, such as \$, \. and \(, because inside a lookbehind they keep their regex meaning. An unescaped $ means end of string and silently matches nothing.

Collect the allowed prefixes in a list and build the pattern from it, joining one lookbehind per prefix. Adding a new currency then means adding a list entry rather than editing a long pattern by hand.

Finally, compare the extracted totals with a few hand-checked rows before trusting the pattern on the full column.

Convert the extracted strings to integers or decimals immediately, and keep the original text column for comparison during the first runs.

If amounts may contain thousands separators, such as $1,250, widen the value part to [\d,]+ and remove the commas before converting.

Common Mistakes

  • Writing (?<=\$|USD ), which mixes widths in one look-behind and fails.
  • Forgetting to escape $, which means end of string.
  • Expecting the prefix in the result.
  • Summing before converting text to numbers.

Quick Summary

  • (?<=...) requires a prefix.
  • Only the value is returned.
  • Each look-behind must be fixed-width.
  • Join several look-behinds with an outer group.

Conclusion

Positive look-behind (?<=...) is a zero-width superpower that gives you precise control over what precedes a match without consuming those characters. In 2026 data science projects it is essential for accurate, context-aware text extraction from logs, reports, and unstructured data. Master positive look-behind (together with its negative counterpart), combine it with pandas vectorized methods, and your regex pipelines will become significantly more precise and professional.

Next steps:

  • Review one of your current regex patterns and add a positive look-behind assertion to make the extraction more context-sensitive

Related Tutorials

Share:
Last updated: March 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices