Lookaround in Regular Expressions

Regular Expressions Mar 20, 2026

Lookaround in Regular Expressions

Lookaround assertions (also called zero-width assertions) let you check what comes before or after a match without consuming those characters. They are one of the most powerful and elegant features of Python’s re module. In data science, lookaround is essential for precise text extraction — for example, finding numbers followed by “USD” but not “EUR”, extracting words that appear before a specific keyword, or validating patterns without including surrounding text.

TL;DR — Four Lookaround Assertions

  • (?=...) → positive lookahead
  • (?!...) → negative lookahead
  • (?<=...) → positive lookbehind
  • (?<!...) → negative lookbehind

1. Basic Lookaround

import re

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

# Positive lookahead: numbers followed by USD
print(re.findall(r"\d+(?= USD)", text))          # ['1250', '1337']

# Negative lookahead: whole numbers NOT followed by EUR
# (\b stops the engine from backtracking to "8" inside "87")
print(re.findall(r"\b\d+\b(?! EUR)", text))      # ['1250', '1337']

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

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 in USD (positive lookahead)
df["usd_amount"] = df["log"].str.extract(r"(\d+(?:,\d+)?(?:\.\d+)?)(?= USD)")

# Example 2: Extract emails that do NOT end with .ru (negative lookahead)
df["valid_email"] = df["log"].str.extract(r"(\S+@\S+\.(?!ru\b)[a-z]+)")

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

print(df[["usd_amount", "valid_email", "sku"]])

3. Advanced Lookaround Combinations

# Lookbehind + lookahead for precise context
text = "Profit: +1250 USD, Loss: -340 EUR"
print(re.findall(r"(?<=[+-])\d+(?= USD)", text))     # ['1250']

# Python's re needs fixed-width lookbehind, so (?<=Price:\s*) raises an error.
# Match the variable part outside the lookbehind and capture the number instead:
print(re.findall(r"Price:\s*(\d+)", "Price:   1250 USD"))   # ['1250']

4. Best Practices in 2026

  • Use positive lookahead (?=...) when you need to “peek ahead” without consuming
  • Use negative lookahead (?!...) to exclude unwanted patterns
  • Remember lookbehind must be fixed-width in Python's re module (the third-party regex module allows variable width)
  • Combine with pandas .str.extract() for vectorized zero-width assertions
  • Keep lookaround assertions simple — they are powerful but can reduce readability if overused

Lookarounds Check Context Without Consuming It

A lookaround tests what comes before or after the current position but does not include that text in the match. There are four kinds: positive look-ahead (?=...), negative look-ahead (?!...), positive look-behind (?<=...) and negative look-behind (?<!...).

Because lookarounds are zero-width, they are ideal when the context decides whether a value counts but should not be part of the extracted value, such as a number followed by a currency or an ID not preceded by a cancelled flag.

One limit is important in Python: the standard re module only allows fixed-width look-behind. A pattern such as (?<=Price:\s*) raises an error, so use a capturing group instead, or the third-party regex module if you truly need variable width.

Worked Example: All Four Lookarounds on One String

We run the four kinds of lookaround on the same short text to compare what each returns.

import re

text = "USD 100, EUR 250, USD 75, fee 30 EUR"

print(re.findall(r"\b\d+\b(?= EUR)", text))   # number followed by EUR
print(re.findall(r"\b\d+\b(?! EUR)", text))   # number not followed by EUR
print(re.findall(r"(?<=USD )\d+", text))      # number after USD
print(re.findall(r"(?<!USD )\b\d+", text))    # number not after USD

The four calls return ['30'], ['100', '250', '75'], ['100', '75'] and ['250', '30']. In every case the currency text is used as context only and never appears in the results.

Practical Tips

Write a one-line comment next to every lookaround that says in plain words what it checks, such as "number followed by EUR". Lookarounds are compact, and a short comment saves the next reader from decoding them symbol by symbol.

Common Mistakes

  • Expecting the lookaround text to be part of the match.
  • Writing variable-width look-behind with the standard re module.
  • Forgetting \b, so the engine backtracks and returns part of a number.
  • Stacking many lookarounds when a simple capturing group would be clearer.

Quick Summary

  • Lookarounds are zero-width checks.
  • Ahead: (?=...) and (?!...); behind: (?<=...) and (?<!...).
  • Python's re needs fixed-width look-behind.
  • Add word boundaries to avoid partial numbers.

Conclusion

Lookaround assertions are zero-width superpowers that let you add context to your matches without including it in the result. In 2026 data science projects they are indispensable for precise, context-aware text extraction from logs, reports, and unstructured data. Master positive/negative lookahead and lookbehind, 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 lookaround assertion to make the extraction more precise

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