Negative Look-Behind in Regular Expressions
Negative look-behind (?<!...) is a zero-width assertion that checks whether a pattern is not preceded by another pattern. It lets you match something only when it is not immediately preceded by a specific context. In data science this is extremely useful for exclusion-based extraction — for example, extracting numbers that are not preceded by “Tax: ”, product codes that do not follow “out of stock”, or emails that are not preceded by a spam indicator.
TL;DR — Negative Look-Behind
(?<!...)→ assert that ... must not precede the match- Zero-width: the look-behind text is not part of the captured result
- Perfect for exclusion-based, context-sensitive extraction
- Works seamlessly with pandas
.str.extract()
1. Basic Negative Look-Behind
import re
text = "Price: 1250 USD, Tax: 87 EUR, Total: 1337 USD"
# Whole numbers NOT preceded by "Tax: "
print(re.findall(r"(?<!Tax: )\b\d+", text)) # ['1250', '1337']
# Product codes NOT preceded by "out of stock "
print(re.findall(r"(?<!out of stock )\bSKU-[A-Z0-9]+", "SKU-ABC123 sold, out of stock SKU-XYZ789")) # ['SKU-ABC123']
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 that are NOT preceded by "Tax: " (negative look-behind)
df["non_tax_amount"] = df["log"].str.extract(r"(?<!Tax: )\b(\d+(?:,\d+)?\.\d+)")
# Example 2: Extract order IDs that are NOT preceded by "Cancelled "
df["valid_order"] = df["log"].str.extract(r"(?<!Cancelled )ORD-(\d+)")
print(df[["non_tax_amount", "valid_order"]])
3. Advanced Negative Look-Behind
# Negative look-behind with a character class
text = "Profit: +1250 USD, Loss: -340 EUR"
print(re.findall(r"(?<![-\d])\d+(?= USD)", text)) # ['1250']
# Exclude a specific prefix
print(re.findall(r"(?<!SKU: )\b[A-Z]{3}\d{3}\b", "SKU: ABC123 sold, XYZ789 in stock")) # ['XYZ789']
4. Best Practices in 2026
- Use negative look-behind
(?<!...)whenever you need to exclude a specific preceding pattern - Combine with capturing groups to extract only the part you want
- Keep look-behind expressions simple and fixed-width where possible for maximum compatibility
- Pre-compile patterns that contain negative look-behind for repeated use on large datasets
- Use with pandas
.str.extract()for vectorized zero-width assertions across entire DataFrames
Negative Look-Behind for Skipping Flagged Values
A negative look-behind (?<!...) matches only where the given text does not come right before the current position. It is useful when a prefix marks values you want to skip, such as a minus sign, a cancelled flag or an escape character.
Like the positive form, it must be fixed-width in Python's re. It also needs boundaries: \d+ after (?<!-) can still match the 5 inside -50, because the 5 is not directly preceded by a minus. Adding \b or (?<![-\d]) fixes this.
Test negative look-behinds on values of several lengths, because the partial-match bug often appears only with multi-digit numbers.
Worked Example: Keeping Only Positive Numbers
We extract numbers that are not negative, comparing a naive pattern with a safer one.
import re
text = "Balance -50, deposit 120, fee -7, bonus 30"
print(re.findall(r"(?<!-)\d+", text))
print(re.findall(r"(?<![-\d])\d+", text))
The naive pattern returns ['0', '120', '30'] because it matches the 0 inside -50. The safer pattern also refuses a digit before the match and returns ['120', '30'].
Practical Tips
Negative lookbehind is often combined with \b or a digit class, as in the example. Make this a habit for numbers, IDs and codes, where a match starting in the middle of a value is a real risk.
When the data is already in a DataFrame, parsing the numbers into a numeric column and filtering with a condition such as df[df["amount"] > 0] is often simpler and faster than a pattern.
Keep a small set of test strings with negative numbers, numbers at the start of a line and numbers next to punctuation, and run the pattern on them after every change. These edge cases reveal most lookbehind bugs.
Write the intent of each negative lookbehind in plain words next to the pattern, for example "numbers not preceded by a minus sign or another digit". This makes the extra digit check easy to understand for the next reader.
Remember that numbers in parentheses, such as (50), are sometimes used for negative values in accounting reports, so check the conventions of your data.
Common Mistakes
- Forgetting that the engine can start a match in the middle of a number.
- Using variable-width patterns inside the look-behind.
- Testing only on single-digit values.
- Using negative look-behind when a parsed numeric column would be simpler.
Quick Summary
- (?<!...) skips values with a given prefix.
- It must be fixed-width in re.
- Block digits too, with (?<![-\d]).
- Test with multi-digit values.
Conclusion
Negative look-behind (?<!...) is a zero-width assertion that gives you powerful exclusion control based on what comes before a match. In 2026 data science projects it is essential for precise, context-aware filtering of text from logs, reports, and unstructured data. Master negative look-behind alongside its positive counterpart, combine them with pandas vectorized methods, and your regex pipelines will become significantly more accurate and professional.
Next steps:
- Review one of your current regex patterns and add a negative look-behind assertion to exclude unwanted preceding text cleanly