Blog / How to Run Sentiment Analysis on News Articles with Python

How to Run Sentiment Analysis on News Articles with Python

NewsMesh Team·2026-01-18·10 min read

News sentiment analysis is one of the most practical applications of natural language processing. Hedge funds use it to gauge market mood before earnings reports. PR teams track it to measure brand perception after a product launch. Researchers rely on it to study media bias across outlets and geographies.

In this tutorial, you will build a complete news sentiment analysis pipeline in Python. You will fetch live articles from the NewsMesh API, analyze their sentiment using two different approaches, compare the results, and visualize sentiment trends across news categories. By the end, you will have a reusable toolkit for monitoring how the news covers any topic.

Prerequisites

You need Python 3.8 or higher. This tutorial uses four packages:

  • requests -- for fetching articles from the NewsMesh API
  • textblob -- for quick, rule-based sentiment analysis
  • transformers -- for transformer-based sentiment classification (Hugging Face)
  • matplotlib -- for visualizing sentiment results

Install them all at once:

pip install requests textblob transformers torch matplotlib

TextBlob also needs its NLTK corpora downloaded:

python -m textblob.download_corpora

You will also need a NewsMesh API key. Sign up for free to get one -- the free tier includes 100 requests per day, which is more than enough for this tutorial.

Step 1: Fetch News Articles from the API

The NewsMesh API provides structured news data from thousands of sources worldwide. The /v1/latest endpoint returns recent articles with filters for category, country, and date range. Check the full API documentation for all available parameters.

Here is a function that fetches the latest articles for a given category:

import requests

API_KEY = "your_api_key_here"
BASE_URL = "https://api.newsmesh.co/v1"

def fetch_articles(category=None, limit=25):
    """Fetch latest articles from the NewsMesh API."""
    params = {
        "apiKey": API_KEY,
        "limit": limit,
    }
    if category:
        params["category"] = category

    response = requests.get(f"{BASE_URL}/latest", params=params)
    response.raise_for_status()
    return response.json()["data"]

Let's test it by fetching 10 technology articles:

articles = fetch_articles(category="technology", limit=10)

for article in articles:
    print(f"[{article['source']}] {article['title']}")
    print(f"  Published: {article['published_date']}")
    print()

Each article in the response includes a title, description, source, category, published_date, and other metadata fields like topics and people. For sentiment analysis, we will primarily work with the title and description fields, since they capture the editorial tone of the article.

Step 2: Quick Sentiment Analysis with TextBlob

TextBlob is a Python library built on top of NLTK and Pattern. It provides a simple .sentiment property that returns two values: polarity (ranging from -1.0 for very negative to +1.0 for very positive) and subjectivity (ranging from 0.0 for very objective to 1.0 for very subjective).

TextBlob uses a lexicon-based approach. It looks up each word in a pre-built dictionary of sentiment scores and combines them. This makes it fast and predictable, but it can miss context, sarcasm, and domain-specific language.

Here is how to analyze sentiment for a batch of articles:

from textblob import TextBlob

def analyze_sentiment_textblob(text):
    """Return polarity score using TextBlob (-1 to +1)."""
    blob = TextBlob(text)
    return blob.sentiment.polarity

# Fetch articles
articles = fetch_articles(category="technology", limit=25)

# Analyze each article
for article in articles:
    title = article["title"]
    description = article.get("description", "") or ""
    combined_text = f"{title}. {description}"

    polarity = analyze_sentiment_textblob(combined_text)

    # Classify into categories
    if polarity > 0.1:
        label = "POSITIVE"
    elif polarity < -0.1:
        label = "NEGATIVE"
    else:
        label = "NEUTRAL"

    print(f"{label:>8} ({polarity:+.3f}) | {title[:80]}")

A typical output might look like this:

POSITIVE (+0.215) | Apple Reports Record Quarter as iPhone Sales Surge Globally
NEGATIVE (-0.180) | Tech Layoffs Continue as Major Companies Cut Thousands of Jobs
 NEUTRAL (+0.050) | EU Regulators Announce New Framework for AI Governance
POSITIVE (+0.300) | Breakthrough Battery Technology Could Double EV Range
NEGATIVE (-0.250) | Cybersecurity Breach Exposes Millions of User Records

The threshold of 0.1 and -0.1 for labeling is a practical choice. News headlines tend to cluster near zero because journalists aim for objectivity. Setting the boundaries too tight will classify nearly everything as positive or negative; setting them too wide will miss genuinely toned headlines.

Limitations of TextBlob

TextBlob works well for simple, direct statements, but it struggles with negation ("not good" sometimes scores as positive because of the word "good"), domain-specific jargon, and headlines that use irony or rhetorical framing. For production applications where accuracy matters, you need a model that understands context.

Step 3: Better Accuracy with Hugging Face Transformers

The Hugging Face transformers library gives you access to pre-trained language models that understand context, word order, and nuance. The distilbert-base-uncased-finetuned-sst-2-english model is a distilled version of BERT, fine-tuned on the Stanford Sentiment Treebank. It outputs a binary classification (POSITIVE or NEGATIVE) along with a confidence score.

from transformers import pipeline

# Load the sentiment analysis pipeline (downloads model on first run)
sentiment_pipeline = pipeline(
    "sentiment-analysis",
    model="distilbert-base-uncased-finetuned-sst-2-english"
)

def analyze_sentiment_transformer(text):
    """Return sentiment label and confidence using a transformer model."""
    # Truncate to 512 tokens (model max length)
    result = sentiment_pipeline(text[:512])[0]
    return result["label"], result["score"]

Now let's compare both approaches on the same set of articles:

articles = fetch_articles(category="business", limit=15)

print(f"{'TextBlob':>12} | {'Transformer':>20} | Title")
print("-" * 80)

for article in articles:
    title = article["title"]
    description = article.get("description", "") or ""
    combined = f"{title}. {description}"

    # TextBlob
    polarity = analyze_sentiment_textblob(combined)
    tb_label = "POS" if polarity > 0.1 else ("NEG" if polarity < -0.1 else "NEU")

    # Transformer
    tf_label, tf_score = analyze_sentiment_transformer(combined)

    print(f"{tb_label:>5} ({polarity:+.2f}) | {tf_label:>8} ({tf_score:.2f}) | {title[:45]}")

In practice, you will notice several differences. The transformer model handles negation correctly: a headline like "Markets fail to recover after steep losses" will be classified as NEGATIVE by the transformer but might get a mixed or neutral score from TextBlob. The transformer also picks up on subtle framing: "Company barely avoids bankruptcy" registers as negative, while TextBlob might focus on the word "avoids" and lean positive.

The tradeoff is speed. TextBlob processes thousands of headlines per second. The transformer model, even with DistilBERT (which is 60% faster than full BERT), processes roughly 50-100 texts per second on CPU. For real-time dashboards processing a handful of articles at a time, the transformer is perfectly fast enough. For batch processing millions of articles, you may want to run inference on a GPU or use TextBlob as a first pass.

Step 4: Analyze Sentiment by Category

One of the most revealing analyses you can run is comparing sentiment across news categories. Are technology articles generally more optimistic than politics coverage? Is sports news more positive than health reporting? Let's find out.

The NewsMesh API supports the following categories: politics, technology, business, sports, entertainment, health, science, lifestyle, environment, and world. We will fetch articles from each and compute aggregate sentiment:

import statistics

CATEGORIES = ["technology", "politics", "business", "sports", "health", "science"]

category_sentiments = {}

for category in CATEGORIES:
    articles = fetch_articles(category=category, limit=25)
    scores = []

    for article in articles:
        title = article["title"]
        description = article.get("description", "") or ""
        combined = f"{title}. {description}"

        label, confidence = analyze_sentiment_transformer(combined)
        # Convert to a numeric score: positive = +confidence, negative = -confidence
        score = confidence if label == "POSITIVE" else -confidence
        scores.append(score)

    avg_score = statistics.mean(scores) if scores else 0
    category_sentiments[category] = {
        "avg_score": avg_score,
        "num_articles": len(scores),
        "positive_pct": sum(1 for s in scores if s > 0) / len(scores) * 100,
        "negative_pct": sum(1 for s in scores if s < 0) / len(scores) * 100,
    }

    print(f"{category:>12}: avg={avg_score:+.3f}, "
          f"pos={category_sentiments[category]['positive_pct']:.0f}%, "
          f"neg={category_sentiments[category]['negative_pct']:.0f}% "
          f"({len(scores)} articles)")

Typical results reveal consistent patterns. Sports and entertainment articles tend to skew positive because they often cover achievements, performances, and celebrations. Politics articles lean negative because they frequently cover conflict, criticism, and policy disputes. Technology coverage falls somewhere in the middle, mixing optimistic product announcements with concerns about layoffs, regulation, and security breaches.

These patterns are not just interesting trivia. If you are building a news reader application, knowing the baseline sentiment per category helps you normalize scores. An article with a sentiment score of -0.3 in sports coverage is unusually negative, while the same score in politics coverage is perfectly average.

Step 5: Visualize the Results

Numbers in a terminal are useful for debugging, but visualizations tell the story. Let's create a bar chart showing average sentiment by category:

import matplotlib.pyplot as plt

categories = list(category_sentiments.keys())
avg_scores = [category_sentiments[c]["avg_score"] for c in categories]

# Color bars by sentiment direction
colors = ["#2ecc71" if s > 0 else "#e74c3c" for s in avg_scores]

fig, ax = plt.subplots(figsize=(10, 6))
bars = ax.barh(categories, avg_scores, color=colors, edgecolor="white", linewidth=0.5)

# Add value labels on bars
for bar, score in zip(bars, avg_scores):
    ax.text(
        bar.get_width() + 0.01 * (1 if score >= 0 else -1),
        bar.get_y() + bar.get_height() / 2,
        f"{score:+.3f}",
        va="center",
        ha="left" if score >= 0 else "right",
        fontsize=10,
    )

ax.set_xlabel("Average Sentiment Score")
ax.set_title("News Sentiment by Category")
ax.axvline(x=0, color="gray", linewidth=0.8, linestyle="--")
ax.set_xlim(-1, 1)
plt.tight_layout()
plt.savefig("sentiment_by_category.png", dpi=150)
plt.show()

You can also visualize the distribution within each category using a box plot to show how much variance exists:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 6))

# Collect per-article scores for each category
all_scores = {}
for category in CATEGORIES:
    articles = fetch_articles(category=category, limit=25)
    scores = []
    for article in articles:
        title = article["title"]
        description = article.get("description", "") or ""
        combined = f"{title}. {description}"
        label, confidence = analyze_sentiment_transformer(combined)
        score = confidence if label == "POSITIVE" else -confidence
        scores.append(score)
    all_scores[category] = scores

ax.boxplot(
    [all_scores[c] for c in CATEGORIES],
    labels=CATEGORIES,
    vert=True,
    patch_artist=True,
    boxprops=dict(facecolor="#3498db", alpha=0.6),
)
ax.set_ylabel("Sentiment Score")
ax.set_title("Sentiment Distribution by News Category")
ax.axhline(y=0, color="gray", linewidth=0.8, linestyle="--")
plt.tight_layout()
plt.savefig("sentiment_distribution.png", dpi=150)
plt.show()

Box plots are especially useful because they reveal outliers. A politics category might have a median close to zero but show a wide spread with extremely negative articles about crises alongside positive articles about diplomatic breakthroughs.

Step 6: Track Sentiment Over Time

A single snapshot of sentiment is useful, but the real value comes from tracking changes over time. Is sentiment around a topic shifting? Did a specific event cause a measurable change in how the news covers an industry?

Here is a script that fetches articles daily and stores the results in a CSV file for long-term analysis:

import csv
import os
from datetime import datetime

CSV_FILE = "sentiment_history.csv"

def record_daily_sentiment():
    """Fetch articles and append daily sentiment averages to CSV."""
    today = datetime.now().strftime("%Y-%m-%d")

    # Check if we already have data for today
    if os.path.exists(CSV_FILE):
        with open(CSV_FILE, "r") as f:
            reader = csv.DictReader(f)
            for row in reader:
                if row["date"] == today:
                    print(f"Data for {today} already recorded. Skipping.")
                    return

    results = []
    for category in CATEGORIES:
        articles = fetch_articles(category=category, limit=25)
        scores = []

        for article in articles:
            title = article["title"]
            description = article.get("description", "") or ""
            combined = f"{title}. {description}"
            label, confidence = analyze_sentiment_transformer(combined)
            score = confidence if label == "POSITIVE" else -confidence
            scores.append(score)

        avg_score = statistics.mean(scores) if scores else 0
        results.append({
            "date": today,
            "category": category,
            "avg_sentiment": round(avg_score, 4),
            "num_articles": len(scores),
            "positive_count": sum(1 for s in scores if s > 0),
            "negative_count": sum(1 for s in scores if s < 0),
        })

    # Write to CSV
    file_exists = os.path.exists(CSV_FILE)
    with open(CSV_FILE, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=results[0].keys())
        if not file_exists:
            writer.writeheader()
        writer.writerows(results)

    print(f"Recorded sentiment data for {today}: {len(results)} categories")

# Run this daily (via cron, schedule library, or manually)
record_daily_sentiment()

After collecting several days of data, you can plot time-series sentiment trends:

import csv
import matplotlib.pyplot as plt
from collections import defaultdict

# Read the CSV
data = defaultdict(lambda: {"dates": [], "scores": []})
with open(CSV_FILE, "r") as f:
    reader = csv.DictReader(f)
    for row in reader:
        cat = row["category"]
        data[cat]["dates"].append(row["date"])
        data[cat]["scores"].append(float(row["avg_sentiment"]))

# Plot each category as a line
fig, ax = plt.subplots(figsize=(12, 6))
for category, values in data.items():
    ax.plot(values["dates"], values["scores"], marker="o", label=category)

ax.set_xlabel("Date")
ax.set_ylabel("Average Sentiment Score")
ax.set_title("News Sentiment Over Time by Category")
ax.legend(loc="upper left", fontsize=9)
ax.axhline(y=0, color="gray", linewidth=0.8, linestyle="--")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("sentiment_over_time.png", dpi=150)
plt.show()

This kind of time-series analysis is where sentiment tracking becomes genuinely actionable. Financial analysts look for sudden drops in business sentiment as an early warning signal. Communications teams watch for negative sentiment spikes around their industry. Researchers use long-running sentiment datasets to study how media narratives evolve during elections, pandemics, or geopolitical crises.

To automate daily collection, you can use a cron job on Linux/macOS:

# Run every day at 8 AM
0 8 * * * /usr/bin/python3 /path/to/sentiment_tracker.py

Or use the Python schedule library for a simpler approach within a long-running script.

Practical Tips

Headline vs. Full-Text Analysis

Analyzing only headlines is faster and often sufficient for tracking broad sentiment trends. Headlines are written to convey the article's core angle in a few words, making them dense with sentiment signals. However, headlines can be misleading -- clickbait titles may suggest negative sentiment while the article body is balanced or even positive. If accuracy is critical, combine the headline with the description field, which the NewsMesh API provides for most articles.

Handling Non-English Articles

The DistilBERT model used in this tutorial is trained on English text only. If you are working with multilingual news, you have two options. First, you can filter articles by language using the language field in the API response. Second, you can use a multilingual sentiment model like nlptown/bert-base-multilingual-uncased-sentiment, which supports English, Dutch, German, French, Spanish, and Italian:

multilingual_pipeline = pipeline(
    "sentiment-analysis",
    model="nlptown/bert-base-multilingual-uncased-sentiment"
)

This model returns star ratings (1 to 5) instead of binary positive/negative labels, so you will need to map those to your sentiment scale.

Batch Processing for Large Volumes

When processing thousands of articles, avoid calling the sentiment pipeline one article at a time. The Hugging Face pipeline accepts a list of strings and processes them in batches internally, which is significantly faster due to GPU parallelism and reduced overhead:

texts = [f"{a['title']}. {a.get('description', '') or ''}" for a in articles]

# Process all at once -- the pipeline handles batching internally
results = sentiment_pipeline(texts, batch_size=32, truncation=True, max_length=512)

for article, result in zip(articles, results):
    print(f"{result['label']} ({result['score']:.2f}) | {article['title'][:60]}")

Setting batch_size=32 means the model processes 32 texts at a time, which maximizes throughput. On a modern CPU, this can process 1,000 articles in under a minute. On a GPU, it takes seconds.

Combining Sentiment with Metadata

The NewsMesh API returns rich metadata alongside each article, including topics, people, and source. You can use these to slice your sentiment analysis in interesting ways. For example, compute the average sentiment for articles mentioning a specific public figure, or compare how different news sources cover the same topic. This kind of entity-level sentiment analysis is the foundation of media monitoring and research workflows.

Conclusion

You now have a working sentiment analysis pipeline that fetches live news data, classifies sentiment using both a simple lexicon-based approach and a state-of-the-art transformer model, and visualizes results across categories and over time.

The key takeaways:

  • TextBlob is fast and good for prototyping, but its accuracy is limited by its word-level approach.
  • Hugging Face transformers provide significantly better accuracy at the cost of speed. For most applications, the tradeoff is worthwhile.
  • Category-level analysis reveals consistent sentiment patterns in the news -- sports and entertainment lean positive, while politics lean negative.
  • Time-series tracking transforms sentiment analysis from a one-off experiment into an ongoing intelligence tool.

From here, there are several directions you can take this project. You could build a real-time sentiment dashboard using Flask or Streamlit. You could fine-tune a sentiment model on news-specific data for even better accuracy. Or you could combine sentiment analysis with the topic and entity extraction features from the NewsMesh API to build a full media monitoring platform.

If you want to explore more ways to work with news data programmatically, check out our guides on building a news aggregator with Python, using news APIs with AI agents and RAG pipelines, and tracking global events with news data.

Ready to start building? Get your free API key and start fetching articles in minutes.

Frequently Asked Questions

What is news sentiment analysis?

News sentiment analysis uses natural language processing (NLP) to determine whether a news article expresses a positive, negative, or neutral tone. It is commonly used in finance, brand monitoring, and media research.

What Python library is best for sentiment analysis?

For quick prototyping, TextBlob or VADER (from NLTK) are simple and effective. For production accuracy, Hugging Face transformers with models like distilbert-base-uncased-finetuned-sst-2-english provide much better results.

Can I do sentiment analysis on news headlines only?

Yes, headline-only analysis is common and often sufficient for tracking overall sentiment trends. Headlines are concise and typically convey the article tone clearly. For deeper analysis, combine headline and description text.

How do I get news articles for sentiment analysis?

Use a news API like NewsMesh to fetch articles programmatically. The API returns structured JSON with titles, descriptions, and metadata. You can filter by category, country, or keyword to focus your analysis.

Related Articles

How to Build a News Aggregator with Python

12 min read

Using News APIs to Power AI Agents and RAG Pipelines

11 min read

Ready to get started?

Start building with NewsMesh. Free tier for development, paid plans from $29/month.

Get Started View Documentation