You need Python installed, and to know how to run a
.py file. If python3 --version in a
terminal prints a version number, you are ready. If it does not — or
if you have never opened a terminal — do
Getting Started with
Python first; it installs Python and runs your first program, and
takes about fifteen minutes. Nothing else is needed: no account, no
payment, no extra software.
What Are Regular Expressions?
Regular expressions (regex) are patterns that describe sets of strings. They let you search, match, and manipulate text with precision that string methods alone can't achieve.
import re
# Find all email addresses in text
text = "Contact alice@example.com or bob@company.org for info"
emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.]+", text)
print(emails) # ['alice@example.com', 'bob@company.org']
Prefix regex patterns with r (e.g., r"\d+") to prevent
Python from interpreting backslashes as escape characters.
Basic Pattern Syntax
# Literal characters match themselves
re.search(r"hello", "say hello world") # Matches "hello"
# Special characters (metacharacters)
. # Any single character (except newline)
^ # Start of string
$ # End of string
\ # Escape special characters
Character Classes
\d # Any digit [0-9]
\D # Any non-digit
\w # Any word character [a-zA-Z0-9_]
\W # Any non-word character
\s # Any whitespace (space, tab, newline)
\S # Any non-whitespace
[abc] # Any of: a, b, or c
[a-z] # Any lowercase letter
[0-9] # Any digit
[^abc] # Any character EXCEPT a, b, c
Quantifiers
* # 0 or more
+ # 1 or more
? # 0 or 1 (optional)
{3} # Exactly 3
{2,5} # Between 2 and 5
{3,} # 3 or more
Core re Module Functions
re.search() — Find First Match
import re
text = "Order #12345 was placed on 2026-03-04"
match = re.search(r"#(\d+)", text)
if match:
print(match.group()) # #12345 (entire match)
print(match.group(1)) # 12345 (first capture group)
print(match.start()) # 6 (position in string)
re.findall() — Find All Matches
text = "Prices: $10.99, $25.50, and $7.00"
prices = re.findall(r"\$\d+\.\d{2}", text)
print(prices) # ['$10.99', '$25.50', '$7.00']
# With groups, findall returns the group contents
numbers = re.findall(r"\$(\d+\.\d{2})", text)
print(numbers) # ['10.99', '25.50', '7.00']
re.sub() — Search and Replace
# Replace phone numbers with [REDACTED]
text = "Call 555-1234 or 555-5678 for support"
cleaned = re.sub(r"\d{3}-\d{4}", "[REDACTED]", text)
print(cleaned) # Call [REDACTED] or [REDACTED] for support
# Use groups in replacement
text = "2026-03-04"
us_format = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\2/\3/\1", text)
print(us_format) # 03/04/2026
re.split() — Split by Pattern
# Split by multiple delimiters
text = "apple, banana; cherry grape"
items = re.split(r"[,;\s]+", text)
print(items) # ['apple', 'banana', 'cherry', 'grape']
Groups and Capturing
import re
# Named groups
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
match = re.search(pattern, "Date: 2026-03-04")
if match:
print(match.group("year")) # 2026
print(match.group("month")) # 03
print(match.group("day")) # 04
print(match.groupdict()) # {'year': '2026', 'month': '03', 'day': '04'}
# Non-capturing group (?:...)
# Groups without capturing (for grouping quantifiers)
pattern = r"(?:https?://)?(?:www\.)?(\w+\.\w+)"
match = re.search(pattern, "Visit www.example.com")
print(match.group(1)) # example.com
Common Patterns
import re
# Email validation (basic)
email_pattern = r"^[\w.+-]+@[\w-]+\.[\w.]+$"
print(bool(re.match(email_pattern, "user@example.com"))) # True
print(bool(re.match(email_pattern, "invalid@"))) # False
# IP address
ip_pattern = r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
text = "Server at 192.168.1.1 responded, backup at 10.0.0.1"
ips = re.findall(ip_pattern, text)
print(ips) # ['192.168.1.1', '10.0.0.1']
# URL extraction
url_pattern = r"https?://[\w./\-?=]+"
text = "Check https://example.com/page?id=1 and http://test.org"
urls = re.findall(url_pattern, text)
print(urls)
# Password validation (8+ chars, uppercase, lowercase, digit)
def check_password(password):
if len(password) < 8:
return False
if not re.search(r"[A-Z]", password):
return False
if not re.search(r"[a-z]", password):
return False
if not re.search(r"\d", password):
return False
return True
Flags
import re
# Case-insensitive matching
re.findall(r"python", "Python PYTHON python", re.IGNORECASE)
# ['Python', 'PYTHON', 'python']
# Multiline (^ and $ match line boundaries)
text = "Line 1\nLine 2\nLine 3"
re.findall(r"^Line \d", text, re.MULTILINE)
# ['Line 1', 'Line 2', 'Line 3']
# Verbose mode (allows comments in patterns)
phone_pattern = re.compile(r"""
(\d{3}) # Area code
[-.\s]? # Optional separator
(\d{3}) # Exchange
[-.\s]? # Optional separator
(\d{4}) # Number
""", re.VERBOSE)
Practical Example: Log Parser
import re
log_lines = [
'2026-03-04 10:30:15 [ERROR] Failed to connect to 192.168.1.100:3306',
'2026-03-04 10:30:16 [INFO] Retrying connection (attempt 2/3)',
'2026-03-04 10:30:17 [ERROR] Connection timeout after 5000ms',
'2026-03-04 10:30:20 [INFO] Connected successfully'
]
pattern = r"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) \[(\w+)\] (.+)"
for line in log_lines:
match = re.match(pattern, line)
if match:
date, time, level, message = match.groups()
if level == "ERROR":
print(f"ERROR at {time}: {message}")
# Extract all IP:port combinations
all_text = "\n".join(log_lines)
connections = re.findall(r"(\d+\.\d+\.\d+\.\d+):(\d+)", all_text)
for ip, port in connections:
print(f" Target: {ip} port {port}")
Now Do It Yourself: Five Steps
Everything above is reference material. This part is a real task from start to finish: you will take a server log file and pull the errors out of it. Every result and every error message below is the exact text Python produced when these commands were run, not an approximation.
Go: open a terminal and move to a folder you can write in — cd ~ puts you in your home folder.
Do: create a file called server.log in any text editor and paste these four lines into it, then save.
2026-03-04 10:30:15 [ERROR] Failed to connect to 192.168.1.100:3306
2026-03-04 10:30:16 [INFO] Retrying connection (attempt 2/3)
2026-03-04 10:30:17 [ERROR] Connection timeout after 5000ms
2026-03-04 10:30:20 [INFO] Connected successfully
You should see: nothing yet — but running cat server.log (or type server.log on Windows) should print the four lines back to you. That confirms the file exists where the terminal is looking.
If not: No such file or directory means the terminal is in a different folder from the one you saved into. Run pwd on Linux or macOS, or cd alone on Windows, to see where you actually are, then cd to the folder holding the file.
Go: in the same folder, start Python with python3.
Do: type these three lines, pressing Enter after each.
import re
text = open("server.log").read()
print(re.search(r"\[ERROR\]", text))
You should see: <re.Match object; span=(20, 27), match='[ERROR]'>. That is not the text itself — it is a match object, which knows both what matched and where. span=(20, 27) means characters 20 to 27 of the file.
If not: if it prints None, your pattern found nothing; check you typed the backslashes before each square bracket. Those backslashes matter enormously: without them, r"[ERROR]" is a character class meaning "any one of E, R, O", and re.findall(r"[ERROR]", text) returns ['E', 'R', 'R', 'O', 'R', 'O'] — single letters, not the word. Escaping them with \[ and \] is what says "I mean literal square brackets".
Go: stay at the >>> prompt.
Do: run this one line.
print(re.findall(r"\d+\.\d+\.\d+\.\d+", text))
You should see: ['192.168.1.100'] — a list, because findall always returns every match rather than the first. This log holds one IP; a real log would give you dozens in the same single line of code.
If not: if you get [], check the escaped dots. A bare . in regex means "any character", so \d+.\d+ would also match 12x34. Writing \. is what pins it to a literal full stop.
Go: leave the prompt with exit() and create a file called errors.py in the same folder.
Do: put these five lines in it, then run python3 errors.py.
import re
for line in open("server.log"):
m = re.match(r"(\S+) (\S+) \[(\w+)\] (.+)", line)
if m and m.group(3) == "ERROR":
print(m.group(2), "->", m.group(4))
You should see: exactly two lines — 10:30:15 -> Failed to connect to 192.168.1.100:3306 and 10:30:17 -> Connection timeout after 5000ms. Each pair of parentheses captured one piece, numbered left to right: date, time, level, message.
If not: if nothing prints, the pattern did not match the line shape — re.match anchors at the start of the string, so a leading space in your file will defeat it. If all four lines print, your if is not filtering; check it reads m.group(3), the third group, which is the level.
Go: create one more file, redact.py.
Do: put these three lines in it and run python3 redact.py.
import re
text = open("server.log").read()
print(re.sub(r"\d+\.\d+\.\d+\.\d+", "[REDACTED]", text).strip())
You should see: all four log lines printed back, with the first now reading Failed to connect to [REDACTED]:3306. The port survives, because the pattern only described the four number groups and the dots between them.
If not: if you see the warning SyntaxWarning: invalid escape sequence '\d', you left off the r before the quotes. Note carefully that it still worked — Python warns and carries on. That is the trap: the habit passes today and breaks in a future Python version. Add the r every time, including in patterns that seem to work without it.
Without scrolling back: why does re.findall(r"[ERROR]", text) return single
letters instead of the word, and what one change fixes it? If you can answer that, you
have understood the difference between a character class and a literal — the single
most common regex mistake there is. Answer: square brackets mean "any one of these
characters"; escape them as r"\[ERROR\]" to match the literal
brackets.
Now do it without the page: pull every time of day
(10:30:15 and the rest) out of the same log into a list. One
findall, one pattern — and remember the r.
Summary
re.search()finds the first match;re.findall()finds all matchesre.sub()does search-and-replace;re.split()splits by pattern\d,\w,\smatch digits, word chars, whitespace+,*,?,{n}control how many times to match- Parentheses
()create capture groups;(?P<name>)creates named groups - Always use raw strings
r"..."for regex patterns - Flags:
re.IGNORECASE,re.MULTILINE,re.VERBOSE
You can now match, extract, and transform text with powerful patterns. Next up: virtual environments and packages — managing dependencies like a pro.