Skip to main content

How to Implement Text Tokenization using Regex in Python

Text tokenization is the process of breaking a stream of text up into words, phrases, symbols, or other meaningful elements called tokens. It is a fundamental step in building compilers, interpreters, and Natural Language Processing (NLP) systems.

While libraries like NLTK exist, building a tokenizer from scratch using Python's re (Regular Expression) module gives you complete control over defining your own grammar and rules.

This guide demonstrates how to build a lexer that categorizes parts of an equation (variables, numbers, operators) into structured tokens.

Defining the Token Structure

A token typically consists of a Type (what is it?) and a Value (what is the actual text?). Python's namedtuple is an excellent choice for this data structure because it is lightweight and immutable.

from collections import namedtuple

# Define a class-like structure for Tokens
Token = namedtuple("Token", ["type", "value"])

# Example instantiation
t = Token("NUM", "42")
print(f"Type: {t.type}, Value: {t.value}")

Output:

Type: NUM, Value: 42

Designing Regex Patterns

To tokenize text, we need to define patterns for every valid element in our language. We map specific names (like NUM or ADD) to regex strings.

import re

# Dictionary mapping Token Names to Regex Patterns
token_specification = {
"NAME": r"[a-zA-Z_][a-zA-Z_0-9]*", # Variable names
"NUM": r"\d+", # Integers
"ADD": r"\+", # Plus sign
"SUB": r"-", # Minus sign
"MUL": r"\*", # Multiply
"DIV": r"/", # Divide
"EQ": r"=", # Assignment
"WS": r"\s+", # Whitespace
}
note

Order matters in regex! If you had a pattern for == and =, you must define == first, otherwise = will match the first half of == prematurely.

Implementing the Tokenizer Logic

We need a function that iterates through the text and matches these patterns.

The Naive vs. Efficient Approach

⛔️ The Incorrect/Inefficient Way: Iterating through the text character by character or using simple .split() often fails to separate operators from numbers without spaces.

text = "val=1+2"
# Splitting by space fails here because there are no spaces
tokens = text.split()
# Result: ['val=1+2'] -> This is just one chunk, not tokenized!

✅ The Correct Way: We compile all patterns into a single Master Regex using Named Groups ((?P<NAME>pattern)). Python's re.finditer scans the text and returns match objects, allowing us to identify which group matched.

import re
from collections import namedtuple

Token = namedtuple("Token", ["type", "value"])

def generate_tokens(text):
# 1. Define patterns
token_specification = {
"NAME": r"[a-zA-Z_][a-zA-Z_0-9]*",
"NUM": r"\d+",
"ADD": r"\+",
"MUL": r"\*",
"EQ": r"=",
"WS": r"\s+",
}

# 2. Join patterns into one master regex: (?P<NAME>...)|(?P<NUM>...)|...
regex = "|".join("(?P<%s>%s)" % pair for pair in token_specification.items())

# 3. Iterate over matches
scanner = re.finditer(regex, text)

for m in scanner:
token_type = m.lastgroup # Gets the name of the matching group (e.g., 'NUM')
token_value = m.group() # Gets the actual text matched (e.g., '10')

# We might want to skip whitespace depending on the use case
# if token_type == 'WS': continue

yield Token(token_type, token_value)
tip

The (?P<name>...) syntax is a regex extension that assigns a name to a capture group. m.lastgroup retrieves this name, allowing us to dynamically determine the token type.

Testing and Output

Let's run the tokenizer on a sample string containing variables, numbers, and operators.

if __name__ == "__main__":
sample_text = "total = 1 + 2 * 3"

# Generate and convert generator to list for printing
tokens = list(generate_tokens(sample_text))

# Pretty print results
for t in tokens:
print(t)

Output:

Token(type='NAME', value='total')
Token(type='WS', value=' ')
Token(type='EQ', value='=')
Token(type='WS', value=' ')
Token(type='NUM', value='1')
Token(type='WS', value=' ')
Token(type='ADD', value='+')
Token(type='WS', value=' ')
Token(type='NUM', value='2')
Token(type='WS', value=' ')
Token(type='MUL', value='*')
Token(type='WS', value=' ')
Token(type='NUM', value='3')

Conclusion

Building a tokenizer in Python relies on leveraging the re module effectively.

  1. Define Rules: Map your token names to specific regex patterns.
  2. Named Groups: Combine patterns using (?P<NAME>pattern) syntax.
  3. Find Iter: Use re.finditer to scan the text linearly and extract tokens based on the matched group name.

This approach creates a robust foundation for building parsers or interpreters.