DEV Community

Cover image for Regex pattern in Python: A beginners guide
Osahenru
Osahenru

Posted on

Regex pattern in Python: A beginners guide

Regular expression (regex) is a sequence of characters that define a search pattern, usually used for text search, text manipulation, and input validation

In this tutorial we are going to see how to validate, clean and extract users data, using code examples to explain the concepts of regular expressions in Python.

At the end of this tutorial, I hope you'll be more confident in using regex in your next project or have a better understanding when you encounter it in a codebase.

^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$
Enter fullscreen mode Exit fullscreen mode

Insight💡: The regex pattern above is used to validate an email address. I know it looks really cryptic, but as we look at examples, you will begin to have a more in-depth understanding.

Let's start by updating our vocabulary with some common metacharacters used in regex.

.   any character except a new line
*   0 or more repetitions
+   1 or more repetitions
?   0 or 1 repetition
{m} m repetitions
{m,n} m-n repetitions
^   matches the start of the string
$   matches the end of the string or just before the newline at the end of the string
\ Used to drop the special meaning of character following it
| Means OR (A|B) A or B
(...)   a group
(?:...) non-capturing version
[]    set of characters
[^]   complementing the set
Enter fullscreen mode Exit fullscreen mode

Now, let's write some code to better understand the regex syntax above, as we will be revisiting this code block frequently.

VALIDATING USERS DATA

Example 1: IP address validator:

Ideally, an IP address should consist of four sets of numbers separated by periods, with each set ranging from 0 to 255 and typically represented with at most three digits (e.g., 192.168.1.1). Each set is called an octet, represented in decimal form. That said, let's see how we can pattern a user's IPv4 address.

import re

def main():
    print(validate(input("IPv4 Address: ")))


def validate(ip):
    matches = re.search(r"^(\d{1,3}\.){3}\d{1,3}$", ip)

    if not matches:
        return False
    return True

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Now, let’s break down this pattern to understand what each syntax does

^ -matches the beginning of the string
\d -numbers only, i.e a letter will return False
{1, 3} -do something either once or 3 times i.e the number can be one - three digits, i.e 0 - 999
\. -a literal dot(.), if we do not use the backslash the re.search function will interpret . as any character except a new line
(\d{1,3}\.) -a group of pattern
{3} -do something 3 times, so we want to do whatever is in the group parenthesis (\d{1,3}\.) 3 times which will match something like this 192.168.1.
\d -digits
{1, 3} -do something either once or 3 times
$ -matches the end of the string or just before the newline
Enter fullscreen mode Exit fullscreen mode

Our code above checks if a user's IP address has four sets of octet numbers. However, it does not check if each octet number is between 0 and 255. Therefore, an IP address like 1.300.600.5 will return True instead of False. To fix this, we can include some extra logic. Modify the code as follows

Example 2: IP address validator:

import re

def main():
    print(validate(input("IPv4 Address: ")))

def validate(ip):
    matches = re.search(r"^(\d{1,3}\.){3}\d{1,3}$", ip)

    if not matches:
        return False

    ip_list = list(map(int, matches.group(0).split(".")))

    for number in ip_list:
        if 0 <= number <= 255:
            return True
        return False


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Let’s update our regex vocabulary to see how we can improve our existing code base

\d    decimal digit
\D    not a decimal digit
\s    whitespace characters
\S    not a whitespace character
\w    word character, as well as numbers and the underscore
\W    not a word character
\b word bound, matches if a word matches the beginning and end of a word
Enter fullscreen mode Exit fullscreen mode

Now, let's see how we might be able to use regex to extract data from a user's input

EXTRACTING DATA

Assuming we have a Facebook URL https://www.facebook.com/johndoe and we want to extract just the username, how might we go about this using our bank of rich regex vocabulary? We can think of various ways to implement this. One straightforward approach is to introduce the use of the re.sub() function.

The re.sub() function in Python is used for replacing substrings that match a specified pattern with a replacement string. It takes three main arguments: re.sub(pattern, replacement, string).

Example 3: Username extractor:

import re


text = 'https://www.facebook.com/johndoe'
matches = re.sub('https://www.facebook.com/', '', text)
print(matches)
Enter fullscreen mode Exit fullscreen mode

Note: This code assumes that every Facebook URL begins with https://www., but you can take it up as a challenge to pattern URLs that begin with http://www. or just www.

I hope at this point it's all beginning to make sense. If not, let's cover some more examples.

CLEANING DATA

Sometimes, phone books append special characters and whitespaces to a phone number. How about we write a pattern to clean the data of a user’s phone number? Let's revisit our rich metacharacter vocabulary.

Assuming we want to get rid of the (+233) and replace it with a 0, how might we go about it? Well, let's take a look at the example below

Example 4: Phone number book:

import re


phone_number = '(+233) 546 07890'
matches = re.sub(r'\(\+233\)', '0', phone_number)

print(matches)

Output: 0 546 07890
Enter fullscreen mode Exit fullscreen mode

We can further improve this code by removing the whitespaces, again, by using the re.sub() function

Example 5: Phone number book

import re

phone_number = '(+233) 546 07890'
matches = re.sub(r'\(\+233\)', '0', phone_number)

rematched_pattern = re.sub(' ', '', matches)
print(rematched_pattern)

Output: 054607890
Enter fullscreen mode Exit fullscreen mode

MORE REGEX FUNCTION

The re.fullmatch() function in regex checks whether the entire string satisfies the pattern. It returns a match object if there is a match and None otherwise. This means that you have to pattern the entire string for the re.fullmatch() function to return a match object; otherwise, it returns None. This is unlike the re.search() function, which looks for the pattern anywhere in the string. Consider the example below:

Example 6 with fullmatch:

import re
text = '123abcd' 
matches = re.fullmatch(r'\d+', text)

print(matches)
Enter fullscreen mode Exit fullscreen mode

When you run this program, you'll notice that the re.fullmatch() function returns None because we only tried to match the digits part of the string with r'\d+'. Now, let's write the same pattern, but using the re.search() function.

Example 7 with search():

import re

text = '123abcd' 
matches = re.search(r'\d+', text)

print(matches)

Output: <re.Match object; span=(0, 3), match='123'>
Enter fullscreen mode Exit fullscreen mode

While the re.fullmatch() function returns None, the re.search() function returns a match object. This illustrates that using the re.fullmatch() function would have been a simpler approach for our IP address example above, as we are mapping an entire string of data.

re.fullmatch() vs. re.match()

The main difference is that re.fullmatch() searches the entire string (from beginning to end) for a pattern, whereas re.match() only searches the beginning for a match. It returns a match object if a match is found, otherwise None.

When to Use fullmatch(), match(), or search()

  • re.fullmatch(): Use when you want to ensure the entire string matches a pattern. For example, to validate a string against a specific format, such as checking if a string is a valid date or a sequence of digits.

  • re.match(): Use when you want to find a match only at the beginning of a string. For example, to validate user input, such as checking if a string starts with a specific prefix.

  • re.search(): Use when you want to find a match anywhere in a string. For example, to check if a string contains a specific pattern, such as a certain word or sequence of characters.

In general, if you are unsure which function to use, re.search() is often a good choice because it is more flexible and can be used to find patterns anywhere in the string. On a surface level, one could argue that re.match() and re.fullmatch() are just shortcuts, as re.search() can perform the functions of both re.match() and re.fullmatch().

COMMON FLAGS

To optimize the functionality of a regex function, you can include flags. We will discuss three common flags used in regex.

re.IGNORECASE(re.I)

This flag ignores case sensitivity in a pattern, allowing uppercase and lowercase letters to be matched interchangeably.

Example 8: without the re.IGNORECASE flag:

import re

text = '123ABCDabcd'
matches = re.search(r'[A-Z]+', text)

print(matches)
Enter fullscreen mode Exit fullscreen mode

The above code prints out a match object <re.Match object; span=(3, 7), match='ABCD'>, omitting lowercase characters. Let's consider another version of the same code where we use the re.IGNORECASE flag and observe the output.

Example 9: with the re.IGNORECASE flag:

import re

text = '123ABCDabcd'

matches = re.search(r'[A-Z]+', text, re.IGNORECASE)
print(matches)
Enter fullscreen mode Exit fullscreen mode

Notice that with the re.IGNORECASE flag, we have a different printout <re.Match object; span=(3, 11), match='ABCDabcd'>, which includes all characters (due to case-insensitivity).

re.MULTILINE (re.M)

This flag allows ^ (caret) and $ (dollar) to match the pattern at the beginning of the string and at the beginning of each newline (\n). It also allows them to match the pattern at the end of the string and at the end of each newline (\n).

Example 10 without the re.MULTILINE flag:

import re

target_str = "Joy lucky number is 75\nTom lucky number is 25"

result = re.findall(r"^\w{3}", target_str)
print(result)

Output: ['Joy']
Enter fullscreen mode Exit fullscreen mode

The code above ignores every third word after a newline (\n). With a modified version like this...

Example 11 with the re.MULTILINE flag:

import re

target_str = "Joy lucky number is 75\nTom lucky number is 25"

result = re.findall(r"^\w{3}", target_str, re.MULTILINE)
print(result)
Output: ['Joy', 'Tom']
Enter fullscreen mode Exit fullscreen mode

we can ignore the new line character and print out every 3 word.

re.DOTALL (re.S)

By default, the . inside a regular expression pattern represents any character (a letter, digit, symbol, or punctuation mark), except the newline character (\n). However, this behavior can be altered using the re.DOTALL flag.

Example 12 without the re.DOTALL flags:

import re

target_str = """ML 
and AI"""
result = re.search(r".+", target_str)
print(result)

Output: <re.Match object; span=(0, 3), match='ML '>
Enter fullscreen mode Exit fullscreen mode

Example 13 with the re.DOTALL flag:

import re

target_str = """ML 
and AI"""

result = re.search(r".+", target_str, re.DOTALL)
print(result)
<re.Match object; span=(0, 10), match='ML \nand AI'>
Enter fullscreen mode Exit fullscreen mode

you can see it included the nextline character to the match object

CONCLUSION

The key to writing a good regex pattern is to visually model the pattern based on the data given and tweak it as you progress, rather than relying solely on memory. Remember to test your pattern against a variety of inputs to ensure it behaves as expected. Please leave a comment below if any questions or for further clarifications. Happy coding!

Top comments (0)