Skip to Content
Course content

110: Lookahead and Lookbehind Assertions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when developers first dive into advanced regex: they treat lookarounds as just another way to group text. They assume that if they use a lookahead or lookbehind, the regex engine "grabs" that text and includes it in the match result, perhaps just in a special way. This is fundamentally wrong.

Lookarounds Aren't Capture Groups

Let's look at a concrete example. Imagine you have a string of prices, like "Price: $100, Discount: $20", and you want to extract just the numbers, but only if they are preceded by a dollar sign. A common instinct is to think a lookbehind acts like a capture group that you just "don't keep."

import re

text = "Price: $100, Discount: $20"
# The "wrong" way (using a capture group)
match = re.search(r"\$(\d+)", text)
print(match.group(0)) # Output: $100
print(match.group(1)) # Output: 100

In the example above, the $ is part of the match (group(0)). To get just the number, you have to explicitly reference group(1). But what if you're using a function like re.findall() or re.sub() and you want the match itself to be only the digits, without the dollar sign ever being part of the "consumed" text? That's where lookarounds come in. They are zero-width assertions. They check a condition, and then the regex engine "resets" its position back to where it was before the check.

Checking the Future with Lookahead

A positive lookahead (?=...) tells the engine: "Make sure the following text matches this pattern, but don't actually move the cursor forward."

I find this incredibly useful for password validation. Say you need a password to contain at least one digit, but you don't want to "consume" the string while checking for that digit, because you still need to check for uppercase letters and symbols starting from the beginning.

import re

password = "SecurePassword123"
# Ensure there is a digit somewhere ahead, then match the whole string
pattern = r"^(?=.*\d).{8,}$"

if re.match(pattern, password):
    print("Password valid!")

In (?=.*\d), the engine looks ahead to see if a digit exists. Once it finds one, it doesn't stay at the position of the digit; it jumps back to the start (the ^) and then continues to match .{8,}. If you had used a standard .*\d, you would have consumed the entire string up to the digit, and your length check would have failed or behaved unpredictably.

Looking Backwards for Context

Positive lookbehind (?<=...) is the mirror image. It checks if a pattern exists immediately before the current position without including it in the result. This is the "clean" way to handle the dollar sign problem we started with.

import re

text = "Price: $100, Discount: $20"
# Match digits ONLY if they are preceded by $
# The $ is checked, but NOT consumed
amounts = re.findall(r"(?<=\$)\d+", text)

print(amounts) # Output: ['100', '20']

Notice that the $ is not in the resulting list. The engine saw the dollar sign, said "Yep, that's there," and then stepped forward to match the digits. If you used \$ \d+, your list would be ['$100', '$20']. It seems like a small distinction, but when you're cleaning data for a database or a CSV, having to manually strip characters from every match is a waste of time and a source of bugs.

One quick warning: in Python's standard re module, lookbehinds must have a fixed width. You can't use + or * inside a lookbehind. If you need variable-width lookbehinds, you'll need to install the regex module from PyPI, but for 90% of software engineering tasks, the standard library's fixed-width version is plenty.




📋 Practical Task

Extracting Amounts from Currency-Tagged Transactions

You are building a financial parser. You have a list of transaction strings where the amount is always followed by a three-letter currency code (e.g., "USD", "EUR", "GBP"), separated by a space. Your goal is to extract only the numeric amounts, but only for transactions that are in "USD".

The Challenge: Use a positive lookahead to ensure the amount is followed by " USD" and a positive lookbehind to ensure the amount is preceded by "Amount: ". The currency code and the label "Amount: " must not be part of the final matched strings.

import re

transactions = [
    "ID: 101, Amount: 150.00 USD, Status: Complete",
    "ID: 102, Amount: 45.50 EUR, Status: Pending",
    "ID: 103, Amount: 200.00 USD, Status: Failed",
    "ID: 104, Amount: 10.00 GBP, Status: Complete"
]

# Your code here:
# Write a regex that finds only the numbers for USD transactions.
# Expected output: ['150.00', '200.00']
usd_amounts = [] 

# TODO: Implement the loop and re.findall() logic
Rating
0 0

There are no comments for now.

to be the first to leave a comment.