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
112: Compiling Patterns for Reuse
If you've been using the re module for a while, you've probably fallen into the habit of calling re.search() or re.match() directly. It’s convenient. You pass in the pattern string and the text you're searching, and Python handles the rest. For a quick script that runs once, this is perfectly fine. But when you're writing production code—especially code that processes thousands of lines of data in a loop—this convenience comes with a hidden tax.
The "Just Call It" Shortcut
Imagine we're building a log parser to find specific error codes in a massive system log. You might write something like this:
import re
def find_errors(log_lines):
errors = []
for line in log_lines:
# We're passing the pattern string every single time
match = re.search(r'ERROR \[Code: (\d+)\]', line)
if match:
errors.append(match.group(1))
return errors
At first glance, this looks clean. However, here is what's happening under the hood: every time re.search() is called, Python has to take that raw string, parse it, compile it into a series of instructions the regex engine can actually execute, and then run it against your text. Now, to be fair, Python does maintain an internal cache of recently used patterns, so it doesn't literally re-compile every single time. But it still has to perform a cache lookup for every single line in your log file. When you're dealing with millions of lines, those lookups add up.
Locking in the Pattern
The professional way to handle this is to "compile" your pattern once and reuse the resulting pattern object. I usually do this at the module level or during the initialization of a class. By using re.compile(), you're telling Python: "I'm going to use this specific regex many times, so please do the hard work of parsing it once and keep the result ready for me."
import re
# We compile the pattern once, outside the loop
ERROR_PATTERN = re.compile(r'ERROR \[Code: (\d+)\]')
def find_errors(log_lines):
errors = []
for line in log_lines:
# We call .search() directly on the compiled object
match = ERROR_PATTERN.search(line)
if match:
errors.append(match.group(1))
return errors
Notice how the logic hasn't really changed, but the execution has. We've moved the overhead of parsing the regex string out of the hot path of our loop. Now, ERROR_PATTERN.search(line) is a direct call to the compiled engine, bypassing the cache lookup entirely.
When the Trade-off Actually Matters
You might be wondering if this is just micro-optimization. In many cases, it is. If you're only running a regex once or twice in the entire lifecycle of your program, re.compile() is actually more verbose and offers zero tangible benefit. I wouldn't bother with it for a simple configuration file check.
But the moment you enter a loop, or the moment you find yourself using the same complex pattern in five different functions, you should compile. Beyond the performance gain, there's a readability win here: by assigning your pattern to a constant like ERROR_PATTERN, you're giving that regex a name. It tells the next developer (which might be you in six months) exactly what that cryptic string of symbols is intended to find, without them having to decipher the regex itself.
📋 Practical Task
Refactoring the Transaction Log Scraper
You have been handed a script that scrapes a financial transaction log to find all "FAILED" transactions along with their Transaction IDs. The current implementation is slow because it recompiles the regex inside a loop. Your task is to refactor this code to use a compiled pattern for better performance.
import re
def get_failed_transactions(logs):
failed_ids = []
# PROBLEM: The regex is defined inside the loop
for entry in logs:
match = re.search(r'STATUS: FAILED \| ID: ([A-Z0-9]+)', entry)
if match:
failed_ids.append(match.group(1))
return failed_ids
# Test data
log_data = [
"STATUS: SUCCESS | ID: AX123",
"STATUS: FAILED | ID: BX456",
"STATUS: SUCCESS | ID: CX789",
"STATUS: FAILED | ID: DX012",
]
print(get_failed_transactions(log_data))
Requirements:
- Move the regex pattern into a compiled object using
re.compile()outside of theget_failed_transactionsfunction. - Update the loop to use the compiled object's
.search()method. - Ensure the output remains the same:
['BX456', 'DX012'].
There are no comments for now.