Square brackets match any one of the characters inside them:
re.fullmatch(r"[abc]", "b") # matches -- 'b' is one of a, b, c
re.fullmatch(r"[A-Za-z]", "Q") # matches -- A-Z or a-z, any single letter
re.fullmatch(r"[A-Za-z0-9_]+", "user_123") # matches -- letters, digits, or underscore, one or more
A-Z inside [] is a range, not the literal characters "A", "-", "Z" — it means "any character from A to Z". You can combine several ranges and individual characters in one set, as in [A-Za-z0-9_] above.
A lookahead checks that something follows the current position, without consuming it (without it counting as part of the match itself):
re.sub(r"\d(?=\d{4})", "*", "1234567890")
# "******7890" -- every digit that has at least 4 more digits after it gets replaced
This is a more advanced tool — you won't need it often, but it's exactly the right tool for "mask everything except the last N characters", which shows up in one of the problems ahead.
re.findall(r"\d+", "a1 b22 c333") # ['1', '22', '333'] -- every non-overlapping match, as a list
re.sub(r"\d", "#", "a1b2c3") # "a#b#c#" -- replace every match with a string
re.findall is what you reach for when you want every match in a string, not just whether one exists. re.sub ("substitute") replaces every match with a replacement string, similar to .replace() but with a pattern instead of a fixed substring.
import re
def is_valid_zip(s):
return re.fullmatch(r"\d{5}", s) is not None
re.fullmatch(...) returns a match object (truthy) if it matches, or None (falsy) if it doesn't — is not None converts that into an explicit True/False, which is the pattern you'll use in most of the validation problems ahead.