Look-Behind Assertions in Regular Expressions
Look-behind assertions let you check what comes before a potential match without consuming those characters. They are zero-width and come in two flavors: positive look-behind (?<=...) and negative look-behind (?<!...). In data science this is extremely powerful for context-aware extraction — for example, finding numbers that are preceded by “Price: ”, product codes that come after “SKU:”, or excluding values that are preceded by unwanted text.
TL;DR — Look-Behind Assertions
(?<=...)→ positive look-behind (must be preceded by ...)(?<!...)→ negative look-behind (must not be preceded by ...)- Zero-width: the look-behind text is not part of the captured result
- Perfect for precise, context-sensitive extraction in logs and reports
1. Basic Look-Behind Assertions
import re
text = "Price: 1250 USD, Tax: 87 EUR, Total: 1337 USD"
# Positive look-behind: numbers preceded by "Price: "
print(re.findall(r"(?<=Price: )\d+", text)) # ['1250']
# Negative look-behind: whole numbers NOT preceded by "Tax: "
print(re.findall(r"(?<!Tax: )\b\d+", text)) # ['1250', '1337']
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 NOT preceded by "Cancelled " (negative look-behind)
df["active_order"] = df["log"].str.extract(r"(?<!Cancelled )ORD-(\d+)")
# Example 3: Extract order IDs only when preceded by "ORD-"
df["order_id"] = df["log"].str.extract(r"(?<=ORD-)(\d+)")
print(df[["price_amount", "active_order", "order_id"]])
3. Advanced Look-Behind Combinations
# Look-behind with a character class (both options are one character wide)
text = "Profit: +1250 USD, Loss: -340 EUR"
print(re.findall(r"(?<=[+-])\d+", text)) # ['1250', '340']
# Look-behind combined with a capturing group
print(re.findall(r"(?<=SKU: )([A-Z0-9-]+)", "SKU: ABC123 sold, SKU: XYZ789 in stock")) # ['ABC123', 'XYZ789']
4. Best Practices in 2026
- Use positive look-behind
(?<=...)when you need to “peek behind” without including the text - Use negative look-behind
(?<!...)to exclude unwanted preceding patterns - Keep look-behind expressions fixed-width and simple (Python's
rerequires a fixed width) - Combine with capturing groups to extract only the part you want
- Use with pandas
.str.extract()for vectorized zero-width assertions across entire DataFrames
Look-Behind and Its Fixed-Width Rule
A look-behind checks the text to the left of the current position. Positive look-behind (?<=...) requires the text to be there, and negative look-behind (?<!...) requires it to be absent.
Python's re module needs to know how far back to look, so every alternative inside a look-behind must have a fixed length. (?<=USD |EUR ) works because both alternatives are four characters, but (?<=Price:\s*) raises re.error because \s* has no fixed width.
When the prefix varies, capture the value with a group instead: Price:\s*(\d+) works for any amount of spacing and str.extract returns only the group.
Worked Example: Fixed-Width Look-Behind and the Group Alternative
We show the error for a variable-width look-behind and then extract the same values with a group.
import re
import pandas as pd
try:
re.compile(r"(?<=Price:\s*)\d+")
except re.error as e:
print("re.error:", e)
s = pd.Series(["Price: 120", "Price:45", "Price: 9"])
print(s.str.extract(r"Price:\s*(\d+)", expand=False).tolist())
print(re.findall(r"(?<=USD |EUR )\d+", "USD 10 EUR 20 GBP 30"))
The compile call fails with "look-behind requires fixed-width pattern". The group version returns ['120', '45', '9'] whatever the spacing, and the fixed-width alternation returns ['10', '20'].
Practical Tips
Prefer a capturing group whenever the prefix varies in length or spacing. It is easier to read than a lookbehind, works in every regex engine, and str.extract returns only the group, so the result is just as clean.
Keep lookbehinds for short, fixed prefixes such as a currency symbol or a field label, where their compact form is easy to understand.
If you move patterns between tools, test them in Python as well. Patterns copied from other engines, such as JavaScript or .NET, may use variable-width lookbehind and fail here with re.error.
Group names work well with this approach too. Writing Price:\s*(?P<price>\d+) gives the extracted column a meaningful name straight away, so the result needs no renaming before it is joined with other data.
Document the choice in a short comment, so nobody later rewrites the group back into a lookbehind.
Common Mistakes
- Using * or + inside a look-behind with the standard re module.
- Mixing alternatives of different lengths inside one look-behind.
- Assuming every regex engine has the same rules; others differ.
- Using look-behind when a simple group is clearer.
Quick Summary
- Look-behind checks text to the left.
- re requires fixed width in look-behind.
- Use a capturing group for variable prefixes.
- The third-party regex module allows variable width.
Conclusion
Look-behind assertions are zero-width superpowers that give you precise control over what precedes a match without consuming those characters. In 2026 data science projects they are essential for accurate, context-aware text extraction from logs, reports, and unstructured data. Master positive and negative look-behind, combine them 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 look-behind assertion to make the extraction more context-sensitive