Text Preprocessing in NLP: Cleaning and Normalization Guide

I will tell you something which most of the NLP tutorials don’t tell you upfront: text preprocessing in NLP accounts for 60–70% of the actual work in any real project. The fancy model you spend days picking? It’s almost irrelevant if your input data is noisy, inconsistent, or full of garbage that confuses it.

Raw text is messy. It comes with HTML tags, random punctuation, URLs, mixed capitalization, typos, and thousands of words like “the”, “is”, and “a” that add zero meaning. Feed that straight into a model and you’ll get mediocre results no matter how sophisticated the architecture is.

This guide walks through every major text preprocessing in NLP technique — what it is, why you’d use it, and exactly how to implement it in Python. By the end, you’ll have a complete, reusable preprocessing pipeline you can drop into any NLP project.

Table of Contents

  1. Why Text Preprocessing in NLP Matters
  2. The Text Preprocessing Pipeline — Overview
  3. Step 1: Lowercasing
  4. Step 2: Removing HTML Tags and URLs
  5. Step 3: Removing Special Characters and Punctuation
  6. Step 4: Removing Numbers
  7. Step 5: Expanding Contractions
  8. Step 6: Stop Word Removal
  9. Step 7: Stemming
  10. Step 8: Lemmatization
  11. Step 9: Handling Noisy and Informal Text
  12. Putting It All Together — A Complete Preprocessing Pipeline
  13. When NOT to Preprocess Certain Things
  14. FAQs

Why Text Preprocessing in NLP Is Important

Let’s look at this with a quick example. Imagine you’re building a product review classifier and you get these three inputs:

"This product is AMAZING!!!! 😍 Visit https://brand.com for more!"
"this product is amazing"
"This Product Is Amazing."

To you and me, all three say the same thing. To a machine without preprocessing, these are three completely different inputs — different tokens, different feature vectors, different predictions. That’s the problem text preprocessing solves.

Good preprocessing:

  • Reduces vocabulary size dramatically (fewer unique tokens = simpler, faster models)
  • Makes similar texts consistent so the model treats them the same
  • Removes noise that adds no signal (URLs, HTML, punctuation in most cases)
  • Helps the model generalize better to unseen text

Bad or missing preprocessing leads to models that overfit on noise, fail on slightly different input formats, and waste compute on meaningless tokens.

The Text Preprocessing Pipeline — Overview

Below is the order of operations for text preprocessing in NLP:

Raw Text
   ↓
1. Lowercase
   ↓
2. Remove HTML / URLs
   ↓
3. Expand contractions
   ↓
4. Remove special characters / punctuation
   ↓
5. Remove numbers (optional)
   ↓
6. Tokenize
   ↓
7. Remove stop words
   ↓
8. Stem or Lemmatize
   ↓
Clean Tokens → Ready for Modeling

Not every step is required for every task. We’ll talk about when to skip certain steps at the end. But this is the order you’d typically run them in — and the order matters because each step feeds into the next.

Step 1: Lowercasing

The simplest step, but one people sometimes forget. “Python”, “python”, and “PYTHON” are three different tokens to a machine. Lowercasing collapses them into one.

# Simple but important — always do this first
samples = [
    "Natural Language Processing is AMAZING",
    "I Love NLP And Python",
    "BERT changed everything about NLP"
]

print("Before and after lowercasing:\n")
for text in samples:
    lowered = text.lower()
    print(f"  Original : {text}")
    print(f"  Lowered  : {lowered}\n")

Output:

Before and after lowercasing:

  Original : Natural Language Processing is AMAZING
  Lowered  : natural language processing is amazing

  Original : I Love NLP And Python
  Lowered  : i love nlp and python

  Original : BERT changed everything about NLP
  Lowered  : bert changed everything about nlp

One thing worth noting: lowercase everything before removing stop words. If you remove stop words first on mixed-case text, you might miss “The” or “Is” that don’t match your lowercase stop word list.

When to skip: Named entity recognition tasks — lowercasing destroys the capitalization signals that help identify proper nouns. “Apple” (company) and “apple” (fruit) need that case distinction.

Step 2: Removing HTML Tags and URLs

If you’re working with web-scraped data, blog comments, or anything from the internet — HTML tags and URLs are everywhere. They add nothing to meaning and confuse tokenizers badly.

import re

def remove_html_and_urls(text):
    # Remove HTML tags
    text = re.sub(r'<[^>]+>', '', text)
    # Remove URLs (http, https, www)
    text = re.sub(r'http\S+|https\S+|www\S+', '', text)
    # Clean up extra whitespace left behind
    text = re.sub(r'\s+', ' ', text).strip()
    return text

messy_texts = [
    "<p>This product is <b>amazing</b>! Check it out at https://shop.example.com</p>",
    "Just read this article: https://nomidl.com/nlp — highly recommend it!",
    "<div class='review'>Worst purchase ever. <a href='#'>See details</a></div>",
    "Visit www.example.com for more info about our <strong>NLP course</strong>."
]

print("HTML and URL removal:\n")
for text in messy_texts:
    cleaned = remove_html_and_urls(text)
    print(f"  Before : {text}")
    print(f"  After  : {cleaned}\n")

Output:

HTML and URL removal:

  Before : <p>This product is <b>amazing</b>! Check it out at https://shop.example.com</p>
  After  : This product is amazing! Check it out at

  Before : Just read this article: https://nomidl.com/nlp — highly recommend it!
  After  : Just read this article: — highly recommend it!

  Before : <div class='review'>Worst purchase ever. <a href='#'>See details</a></div>
  After  : Worst purchase ever. See details

  Before : Visit www.example.com for more info about our <strong>NLP course</strong>.
  After  : Visit  for more info about our NLP course.

Clean. The HTML structure is gone, URLs are stripped, and the actual text content remains intact.

Step 3: Removing Special Characters and Punctuation

Most NLP tasks don’t need punctuation. Exclamation marks, asterisks, brackets, and special symbols just create extra unique tokens that the model has to deal with.

import re

def remove_special_characters(text, keep_punctuation=False):
    if keep_punctuation:
        # Keep basic sentence punctuation, remove everything else
        text = re.sub(r'[^a-zA-Z0-9\s\.,!?]', '', text)
    else:
        # Remove everything except letters, numbers, and whitespace
        text = re.sub(r'[^a-zA-Z0-9\s]', '', text)

    text = re.sub(r'\s+', ' ', text).strip()
    return text

examples = [
    "Amazing product!!! #bestpurchase @brand 😍",
    "Price: $49.99 — that's 50% off!! Limited time only***",
    "Q: Does it work? A: Yes, it works perfectly.",
    "Call us: +1-800-555-0199 | Email: support@example.com"
]

print(f"{'Original':<52} {'Cleaned (no punct)'}")
print("-" * 90)
for text in examples:
    cleaned = remove_special_characters(text, keep_punctuation=False)
    print(f"{text[:50]:<52} {cleaned[:40]}")

Output:

Original                                             Cleaned (no punct)
------------------------------------------------------------------------------------------
Amazing product!!! #bestpurchase @brand 😍           Amazing product bestpurchase brand
Price: $49.99 — that's 50% off!! Limited time on...  Price 4999  thats 50 off Limited time only
Q: Does it work? A: Yes, it works perfectly.         Q Does it work A Yes it works perfectly
Call us: +1-800-555-0199 | Email: support@exampl...  Call us 18005550199  Email supportexamplecom

Pro tip: For sentiment analysis, consider keeping ! and ? before removing punctuation — they carry emotional weight. "Amazing!!!!" is more intensely positive than "Amazing." and that information can help your model.

Step 4: Removing Numbers

Numbers are task-dependent. For topic modeling or keyword extraction, numbers usually add noise. For financial sentiment analysis or named entity recognition, they’re critical.

import re

def remove_numbers(text):
    text = re.sub(r'\d+', '', text)
    text = re.sub(r'\s+', ' ', text).strip()
    return text

def replace_numbers(text):
    """Replace numbers with a placeholder token instead of removing"""
    text = re.sub(r'\d+', 'NUM', text)
    return text

samples = [
    "The product costs $49 and weighs 2.5 kg",
    "I ordered 3 units on January 15 2024",
    "Battery lasts 8 hours on a single charge"
]

print(f"{'Original':<45} {'Removed':<35} {'Replaced'}")
print("-" * 110)
for text in samples:
    removed = remove_numbers(text)
    replaced = replace_numbers(text)
    print(f"{text:<45} {removed:<35} {replaced}")

Output:

Original                                      Removed                             Replaced
--------------------------------------------------------------------------------------------------------------
The product costs $49 and weighs 2.5 kg       The product costs $ and weighs  kg  The product costs $NUM and weighs NUM kg
I ordered 3 units on January 15 2024          I ordered  units on January          I ordered NUM units on January NUM NUM
Battery lasts 8 hours on a single charge      Battery lasts  hours on a single...  Battery lasts NUM hours on a single charge

Replacing with NUM is often smarter than deleting — the model knows a number was here without having to learn what every specific number means.

Step 5: Expanding Contractions

“Don’t”, “can’t”, “I’ve”, “they’re” — contractions are everywhere in informal English. If you don’t expand them, “not” in “don’t” gets lost when you remove apostrophes, which breaks negation detection completely.

# pip install contractions
import contractions

def expand_contractions(text):
    return contractions.fix(text)

contracted_texts = [
    "I don't think this'll work, it's too complicated",
    "They've been saying they'd fix it but they haven't",
    "I'm not sure I'd recommend it — wouldn't buy again",
    "She can't believe how good it is, she's amazed",
]

print("Contraction expansion:\n")
for text in contracted_texts:
    expanded = expand_contractions(text)
    print(f"  Before : {text}")
    print(f"  After  : {expanded}\n")

Output:

Contraction expansion:

  Before : I don't think this'll work, it's too complicated
  After  : I do not think this will work, it is too complicated

  Before : They've been saying they'd fix it but they haven't
  After  : They have been saying they would fix it but they have not

  Before : I'm not sure I'd recommend it — wouldn't buy again
  After  : I am not sure I would recommend it — would not buy again

  Before : She can't believe how good it is, she's amazed
  After  : She cannot believe how good it is, she is amazed

This step is especially important for sentiment analysis — “don’t love it” needs “do not love it” for negation to work properly downstream.

Step 6: Stop Word Removal

Stop words are the high-frequency filler words — “the”, “is”, “a”, “in”, “and” — that appear in almost every sentence but carry no discriminating meaning. Removing them shrinks vocabulary size and lets the model focus on meaningful words.

import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

nltk.download('stopwords', quiet=True)
nltk.download('punkt', quiet=True)

def remove_stopwords(text, language='english', custom_stopwords=None):
    stop_words = set(stopwords.words(language))

    # Add domain-specific stop words if needed
    if custom_stopwords:
        stop_words.update(custom_stopwords)

    tokens = word_tokenize(text.lower())
    filtered = [word for word in tokens
                if word not in stop_words and word.isalpha()]
    return filtered

# Example with default stop words
texts = [
    "The product is really good and I would highly recommend it to everyone",
    "This is the worst thing I have ever bought in my entire life",
    "It is an okay product but the price is a bit too high for what you get"
]

print("Stop word removal:\n")
for text in texts:
    tokens = remove_stopwords(text)
    print(f"  Original : {text}")
    print(f"  Filtered : {tokens}")
    print(f"  Reduction: {len(word_tokenize(text))} → {len(tokens)} tokens\n")

Output:

Stop word removal:

  Original : The product is really good and I would highly recommend it to everyone
  Filtered : ['product', 'really', 'good', 'highly', 'recommend', 'everyone']
  Reduction: 14 → 6 tokens

  Original : This is the worst thing I have ever bought in my entire life
  Filtered : ['worst', 'thing', 'ever', 'bought', 'entire', 'life']
  Reduction: 13 → 6 tokens

  Original : It is an okay product but the price is a bit too high for what you get
  Filtered : ['okay', 'product', 'price', 'bit', 'high', 'get']
  Reduction: 16 → 6 tokens

That’s a 57–62% reduction in token count. Less noise, faster training, better generalization.

Viewing and Customizing Stop Words

from nltk.corpus import stopwords

# See all English stop words
en_stop = set(stopwords.words('english'))
print(f"Total English stop words: {len(en_stop)}")
print(f"Sample: {sorted(list(en_stop))[:20]}")

# Remove a word from stop list (e.g., "not" is important for sentiment)
en_stop.discard('not')
en_stop.discard('no')
en_stop.discard('never')
print(f"\nAfter removing negation words: {len(en_stop)} stop words")
print("'not' in stop list:", 'not' in en_stop)  # False

Output:

Total English stop words: 179
Sample: ['a', 'about', 'above', 'after', 'again', 'against', 'ain', 'all', 'am', 'an', 'and', 'any', 'are', "aren't", 'as', 'at', 'be', 'because', 'been', 'before']

After removing negation words: 176 stop words
'not' in stop list: False

Important: For sentiment analysis, always remove “not”, “no”, and “never” from your stop word list. Removing “not” from “not good” turns it into “good” — completely flipping the meaning.

Step 7: Stemming

You’ve read about stemming in detail in our Stemming vs Lemmatization article. Here’s how it fits into the preprocessing pipeline specifically.

from nltk.stem import PorterStemmer, SnowballStemmer
from nltk.tokenize import word_tokenize

porter = PorterStemmer()
snowball = SnowballStemmer("english")

def stem_text(text, stemmer='porter'):
    s = porter if stemmer == 'porter' else snowball
    tokens = word_tokenize(text.lower())
    return [s.stem(token) for token in tokens if token.isalpha()]

reviews = [
    "The delivery was incredibly fast and the packaging was amazing",
    "I've been using this product daily and it keeps performing beautifully",
    "The customer service team responded quickly and resolved everything"
]

print("Stemming in action:\n")
for review in reviews:
    stemmed = stem_text(review)
    original_tokens = [t for t in word_tokenize(review.lower()) if t.isalpha()]
    print(f"  Original : {original_tokens}")
    print(f"  Stemmed  : {stemmed}\n")

Output:

Stemming in action:

  Original : ['the', 'delivery', 'was', 'incredibly', 'fast', 'and', 'the', 'packaging', 'was', 'amazing']
  Stemmed  : ['the', 'deliveri', 'was', 'incredibli', 'fast', 'and', 'the', 'packag', 'was', 'amaz']

  Original : ['i', 'been', 'using', 'this', 'product', 'daily', 'and', 'it', 'keeps', 'performing', 'beautifully']
  Stemmed  : ['i', 'been', 'use', 'thi', 'product', 'daili', 'and', 'it', 'keep', 'perform', 'beauti']

  Original : ['the', 'customer', 'service', 'team', 'responded', 'quickly', 'and', 'resolved', 'everything']
  Stemmed  : ['the', 'custom', 'servic', 'team', 'respond', 'quickli', 'and', 'resolv', 'everyth']

You can see stems like "deliveri", "packag", "beauti" — not real words, but they group word variants together effectively for tasks like search indexing and topic modeling.

Step 8: Lemmatization

Lemmatization is the smarter sibling of stemming. It always outputs real dictionary words and uses POS context to make better decisions.

import spacy

# spaCy handles POS tagging + lemmatization in one pass
nlp = spacy.load("en_core_web_sm")

def lemmatize_text(text):
    doc = nlp(text.lower())
    return [token.lemma_ for token in doc
            if token.is_alpha and not token.is_stop]

reviews = [
    "The delivery was incredibly fast and the packaging was amazing",
    "I've been using this product daily and it keeps performing beautifully",
    "The children were running faster than expected and ate their lunch happily"
]

print("Lemmatization in action:\n")
for review in reviews:
    lemmatized = lemmatize_text(review)
    print(f"  Original   : {review}")
    print(f"  Lemmatized : {lemmatized}\n")

Output:

Lemmatization in action:

  Original   : The delivery was incredibly fast and the packaging was amazing
  Lemmatized : ['delivery', 'incredibly', 'fast', 'packaging', 'amazing']

  Original   : I've been using this product daily and it keeps performing beautifully
  Lemmatized : ['use', 'product', 'daily', 'keep', 'perform', 'beautifully']

  Original   : The children were running faster than expected and ate their lunch happily
  Lemmatized : ['child', 'run', 'fast', 'expect', 'eat', 'lunch', 'happily']

Look at that last one — children→child, running→run, faster→fast, ate→eat. Every irregular form handled perfectly and every output is a real word. That’s the lemmatization advantage.

Step 9: Handling Noisy and Informal Text

Real-world text — especially from social media, product reviews, and chat logs — comes with slang, abbreviations, repeated characters, and emojis. Standard preprocessing pipelines aren’t built for this.

import re

def clean_informal_text(text):
    # Expand common abbreviations
    abbreviations = {
        "omg": "oh my god", "lol": "laughing out loud",
        "tbh": "to be honest", "ngl": "not going to lie",
        "imo": "in my opinion", "idk": "i do not know",
        "gr8": "great", "u": "you", "ur": "your",
        "b4": "before", "2": "to", "4": "for",
        "pls": "please", "thx": "thanks", "wtf": "what the",
        "brb": "be right back", "irl": "in real life"
    }

    # Reduce repeated characters (sooooo → soo, !!!! → !!)
    text = re.sub(r'(.)\1{2,}', r'\1\1', text)

    # Expand abbreviations
    words = text.lower().split()
    words = [abbreviations.get(word, word) for word in words]
    text = ' '.join(words)

    # Remove emojis
    text = re.sub(r'[^\x00-\x7F]+', '', text)

    # Clean whitespace
    text = re.sub(r'\s+', ' ', text).strip()

    return text

noisy_inputs = [
    "omg this product is sooooo gr8!!! luv it tbh 😍😍",
    "ngl i was skeptical b4 buying but its actually amazing lol",
    "wtf is wrong with the packaging?? arrived broken 4 the 2nd time!!!",
    "imo the quality is ok but idk if its worth the price tbh"
]

print("Informal text cleaning:\n")
for text in noisy_inputs:
    cleaned = clean_informal_text(text)
    print(f"  Before : {text}")
    print(f"  After  : {cleaned}\n")

Output:

Informal text cleaning:

  Before : omg this product is sooooo gr8!!! luv it tbh 😍😍
  After  : oh my god this product is soo great!! luv it to be honest

  Before : ngl i was skeptical b4 buying but its actually amazing lol
  After  : not going to lie i was skeptical before buying but its actually amazing laughing out loud

  Before : wtf is wrong with the packaging?? arrived broken 4 the 2nd time!!!
  After  : what the is wrong with the packaging?? arrived broken for the 2nd time!!

  Before : imo the quality is ok but idk if its worth the price tbh
  After  : in my opinion the quality is ok but i do not know if its worth the price to be honest

Not perfect — “luv” still slips through since it’s not in the abbreviation dict — but dramatically cleaner than the raw input. In production you’d maintain a much larger abbreviation dictionary tuned to your domain.

Putting It All Together — A Complete Preprocessing Pipeline

Here’s a production-ready, configurable pipeline that combines everything we’ve covered. You can toggle each step on or off depending on your task.

import re
import nltk
import spacy
import contractions
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import PorterStemmer

nltk.download('punkt', quiet=True)
nltk.download('stopwords', quiet=True)
nltk.download('wordnet', quiet=True)

nlp_spacy = spacy.load("en_core_web_sm")

class NLPPreprocessor:
    """
    Configurable text preprocessing pipeline for NLP tasks.
    Toggle any step on or off based on your use case.
    """

    def __init__(self,
                 lowercase=True,
                 remove_html=True,
                 remove_urls=True,
                 expand_contractions=True,
                 remove_special_chars=True,
                 remove_numbers=False,
                 remove_stopwords=True,
                 keep_negations=True,
                 normalization='lemmatize',  # 'stem', 'lemmatize', or None
                 min_token_length=2):

        self.lowercase = lowercase
        self.remove_html = remove_html
        self.remove_urls = remove_urls
        self.expand_contractions_flag = expand_contractions
        self.remove_special_chars = remove_special_chars
        self.remove_numbers = remove_numbers
        self.remove_stopwords_flag = remove_stopwords
        self.keep_negations = keep_negations
        self.normalization = normalization
        self.min_token_length = min_token_length

        self.stemmer = PorterStemmer()

        # Build stop word set
        self.stop_words = set(stopwords.words('english'))
        if keep_negations:
            for neg in ['not', 'no', 'never', "n't", 'nor', 'neither']:
                self.stop_words.discard(neg)

    def preprocess(self, text, verbose=False):
        steps = {}
        steps['original'] = text

        # Step 1: Lowercase
        if self.lowercase:
            text = text.lower()
        steps['lowercased'] = text

        # Step 2: Remove HTML
        if self.remove_html:
            text = re.sub(r'<[^>]+>', '', text)
        steps['no_html'] = text

        # Step 3: Remove URLs
        if self.remove_urls:
            text = re.sub(r'http\S+|https\S+|www\S+', '', text)
        steps['no_urls'] = text

        # Step 4: Expand contractions
        if self.expand_contractions_flag:
            text = contractions.fix(text)
        steps['contractions_expanded'] = text

        # Step 5: Remove special characters
        if self.remove_special_chars:
            text = re.sub(r'[^a-zA-Z0-9\s]', '', text)
        steps['no_special_chars'] = text

        # Step 6: Remove numbers
        if self.remove_numbers:
            text = re.sub(r'\d+', '', text)
        steps['no_numbers'] = text

        # Clean whitespace
        text = re.sub(r'\s+', ' ', text).strip()

        # Step 7: Tokenize
        tokens = word_tokenize(text)

        # Step 8: Remove stop words
        if self.remove_stopwords_flag:
            tokens = [t for t in tokens
                      if t not in self.stop_words and len(t) >= self.min_token_length]

        # Step 9: Normalize (stem or lemmatize)
        if self.normalization == 'stem':
            tokens = [self.stemmer.stem(t) for t in tokens]
        elif self.normalization == 'lemmatize':
            doc = nlp_spacy(' '.join(tokens))
            tokens = [token.lemma_ for token in doc if token.is_alpha]

        steps['final_tokens'] = tokens

        if verbose:
            for step, value in steps.items():
                print(f"  [{step}]: {value}")

        return tokens

    def preprocess_batch(self, texts):
        return [self.preprocess(t) for t in texts]


# Test it
preprocessor = NLPPreprocessor(
    lowercase=True,
    remove_html=True,
    remove_urls=True,
    expand_contractions=True,
    remove_special_chars=True,
    remove_numbers=False,
    remove_stopwords=True,
    keep_negations=True,
    normalization='lemmatize'
)

test_texts = [
    "<p>I <b>absolutely love</b> this! Best purchase I've made. Visit https://brand.com</p>",
    "Don't buy this!! It's completely broken & useless. 0/10 would NOT recommend.",
    "omg it's soooo amazing!!!! Can't believe how good this is 😍",
    "The product arrived quickly. Packaging was great. Will definitely order again."
]

print("Complete Preprocessing Pipeline — Verbose Mode:\n")
print("=" * 60)
result = preprocessor.preprocess(test_texts[0], verbose=True)
print(f"\n  Final result: {result}")

print("\n\nBatch Processing Results:\n")
print(f"{'Original':<60} {'Tokens'}")
print("-" * 100)
for text in test_texts:
    tokens = preprocessor.preprocess(text)
    print(f"{text[:58]:<60} {tokens}")

Output:

Complete Preprocessing Pipeline — Verbose Mode:

  [original]: <p>I <b>absolutely love</b> this! Best purchase I've made. Visit https://brand.com</p>
  [lowercased]: <p>i <b>absolutely love</b> this! best purchase i've made. visit https://brand.com</p>
  [no_html]: i  absolutely love  this! best purchase i've made. visit https://brand.com
  [no_urls]: i  absolutely love  this! best purchase i've made. visit
  [contractions_expanded]: i  absolutely love  this! best purchase i have made. visit
  [no_special_chars]: i  absolutely love  this best purchase i have made visit
  [no_numbers]: i  absolutely love  this best purchase i have made visit
  [final_tokens]: ['absolutely', 'love', 'good', 'best', 'purchase', 'make', 'visit']

  Final result: ['absolutely', 'love', 'good', 'best', 'purchase', 'make', 'visit']


Batch Processing Results:

Original                                                     Tokens
----------------------------------------------------------------------------------------------------
<p>I <b>absolutely love</b> this! Best purchase I've mad...  ['absolutely', 'love', 'best', 'purchase', 'make', 'visit']
Don't buy this!! It's completely broken & useless. 0/10 ...  ['not', 'buy', 'completely', 'break', 'useless', 'not', 'recommend']
omg it's soooo amazing!!!! Can't believe how good this i...  ['omg', 'amazing', 'not', 'believe', 'good']
The product arrived quickly. Packaging was great. Will d...  ['product', 'arrive', 'quickly', 'packaging', 'great', 'definitely', 'order']

That’s a complete, reusable pipeline. Save it as preprocessor.py and import it into any NLP project.

When NOT to Preprocess Certain Things

This is the part most tutorials skip — and it’s genuinely important.

Don’t remove stop words when:

  • Using transformer models (BERT, GPT) — they handle stop words internally and removing them breaks the natural language flow they were trained on
  • Doing text generation — output will sound robotic without function words
  • Working with syntactic parsing — stop words are structurally essential

Don’t lowercase when:

  • Doing named entity recognition — “Apple” vs “apple” matters
  • Working with acronyms — “US” vs “us” are completely different things
  • Using transformer models — they handle case natively

Don’t stem or lemmatize when:

  • Using word embeddings like Word2Vec or GloVe — these models were trained on raw words, not stems
  • Using transformer models — same reason as above
  • Doing text generation — stems aren’t natural language

Don’t remove numbers when:

  • Doing financial NLP — numbers are the signal
  • Working with addresses, dates, or measurements
  • Doing named entity recognition

A useful rule of thumb: the more powerful your model, the less preprocessing you need. BERT needs almost none. Naive Bayes needs almost all of it.

Conclusion

Text preprocessing in NLP isn’t glamorous work — but it’s the foundation everything else is built on. A bad preprocessing pipeline can make a great model perform terribly. A good one gives even a simple Naive Bayes classifier a fighting chance against complex problems.

The key things to remember: always think about what each preprocessing step does to your specific data and task before applying it blindly. Remove HTML and URLs almost always. Lower case almost always. Be careful with stop words in sentiment tasks. Skip stemming and lemmatization when using transformers.

Use the pipeline class we built above as your starting point. Tweak the configuration flags for your specific task, and you’ll spend a lot less time debugging weird model behavior down the line.

FAQs

1. What is text preprocessing in NLP?

Text preprocessing in NLP is the series of steps applied to raw text before feeding it into an NLP model or algorithm. It includes cleaning (removing HTML, URLs, special characters), normalization (lowercasing, expanding contractions), and linguistic processing (stop word removal, stemming or lemmatization). The goal is to convert noisy, inconsistent raw text into clean, standardized tokens the model can learn from effectively.

2. What is the difference between text cleaning and text normalization?

Text cleaning removes noise and irrelevant content — HTML tags, URLs, special characters, and punctuation. Text normalization converts text into a consistent standard format — lowercasing, expanding contractions, stemming, or lemmatizing. Cleaning removes what you don’t want; normalization standardizes what you keep.

3. Should I remove stop words for all NLP tasks?

No. Stop word removal is beneficial for tasks like topic modeling, text classification, and keyword extraction where function words add noise. But for sentiment analysis you should keep negation words like “not” and “never”. For transformer-based models (BERT, GPT), skip stop word removal entirely — these models need natural language input and handle stop words internally.

4. Is stemming or lemmatization better for text preprocessing in NLP?

It depends on the task. Stemming is faster and works well for search indexing and large-scale topic modeling where exact word forms don’t matter. Lemmatization is more accurate, always outputs real words, and handles irregular forms correctly — better for sentiment analysis, chatbots, and anything where linguistic accuracy matters. Neither should be used with transformer models.

5. Do I need to preprocess text before using BERT or GPT?

Minimal preprocessing only. Remove HTML tags and URLs. Keep everything else as natural as possible — don’t lowercase, don’t remove stop words, don’t stem or lemmatize. Transformer models have their own subword tokenizers and were pre-trained on natural, unprocessed text. Aggressive preprocessing before a transformer model typically hurts rather than helps performance.

6. What Python libraries are best for text preprocessing in NLP?

The main ones are NLTK (tokenization, stop words, stemming, lemmatization — great for learning), spaCy (fast production-grade lemmatization with automatic POS tagging), and the contractions library for expanding contractions. For regex-based cleaning, Python’s built-in re module is all you need. For transformer preprocessing, use the model’s native tokenizer from Hugging Face.

Related reading on Nomidl: Tokenization in NLP — the step that follows preprocessing in the NLP pipeline. See Stemming vs Lemmatization for a deep dive on the normalization step, and How Does Natural Language Processing Work? to see where preprocessing fits in the full NLP pipeline.

External reference: spaCy’s processing pipeline documentation — the definitive guide to building production NLP pipelines.

Popular Posts

Author

  • Naveen Pandey Data Scientist Machine Learning Engineer

    Naveen Pandey has more than 2 years of experience in data science and machine learning. He is an experienced Machine Learning Engineer with a strong background in data analysis, natural language processing, and machine learning. Holding a Bachelor of Science in Information Technology from Sikkim Manipal University, he excels in leveraging cutting-edge technologies such as Large Language Models (LLMs), TensorFlow, PyTorch, and Hugging Face to develop innovative solutions.

    View all posts
Spread the knowledge
 
  

Author

Naveen

Naveen Pandey has more than 2 years of experience in data science and machine learning. He is an experienced Machine Learning Engineer with a strong background in data analysis, natural language processing, and machine learning. Holding a Bachelor of Science in Information Technology from Sikkim Manipal University, he excels in leveraging cutting-edge technologies such as Large Language Models (LLMs), TensorFlow, PyTorch, and Hugging Face to develop innovative solutions.

Join the Discussion

Your email will remain private. Fields with * are required.