Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
110: Lookahead and Lookbehind Assertions
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
There are no comments for now.