Skip to content

News Aggregator

RSS is the quiet backbone of the web — nearly every news site, blog, and podcast publishes a machine-readable feed. A news aggregator turns those feeds into one personal front page. In this tutorial you build a Tkinter app that fetches articles from several RSS categories with feedparser, lists the headlines, shows summaries on click, and saves favorites to a file. Then you make it feel professional: background fetching so the UI never freezes, full-text search, duplicate removal across feeds, and persistent JSON storage.

You will leave understanding:

  • What RSS/Atom feeds are and how feedparser normalizes them.
  • The fetch → list → detail → save flow common to every reader app.
  • Why network calls must move off the main thread in a GUI.
  • How to dedupe and search across multiple sources.
  • Python 3.6 or above.
  • A text editor or IDE.
  • feedparser: pip install feedparser.
  • Tkinter (bundled with Python).
  • Basic understanding of dictionaries and lists.
  1. Create a folder named news-aggregator.
  2. Inside it, create news_aggregator.py.
  3. Install the dependency: pip install feedparser.
news_aggregator.py pch.viewSource
news_aggregator.py
"""
News Aggregator

A news aggregator application with the following features:
- Fetch news articles from multiple sources using RSS feeds or APIs
- Categorize news articles by topic (e.g., technology, sports, etc.)
- Search functionality to find specific articles
- Save favorite articles for later reading
"""

import feedparser
from tkinter import Tk, Label, Entry, Button, Listbox, Scrollbar, StringVar, messagebox


class NewsAggregator:
    def __init__(self, root):
        self.root = root
        self.root.title("News Aggregator")

        self.feeds = {
            "Technology": "https://rss.nytimes.com/services/xml/rss/nyt/Technology.xml",
            "Sports": "https://rss.nytimes.com/services/xml/rss/nyt/Sports.xml",
            "World": "https://rss.nytimes.com/services/xml/rss/nyt/World.xml",
        }

        self.articles = []
        self.selected_feed = StringVar(value="Technology")

        self.setup_ui()

    def setup_ui(self):
        """Set up the user interface."""
        Label(self.root, text="Select Category:").pack(pady=5)
        for category in self.feeds.keys():
            Button(self.root, text=category, command=lambda c=category: self.fetch_articles(c)).pack(pady=2)

        Label(self.root, text="Articles:").pack(pady=5)
        self.article_list = Listbox(self.root, width=80, height=20)
        self.article_list.pack(pady=5)

        scrollbar = Scrollbar(self.article_list)
        scrollbar.pack(side="right", fill="y")

        Button(self.root, text="View Article", command=self.view_article).pack(pady=5)
        Button(self.root, text="Save to Favorites", command=self.save_to_favorites).pack(pady=5)

    def fetch_articles(self, category):
        """Fetch articles from the selected category."""
        feed_url = self.feeds.get(category)
        if not feed_url:
            messagebox.showerror("Error", "Invalid category selected.")
            return

        try:
            feed = feedparser.parse(feed_url)
            self.articles = feed.entries

            self.article_list.delete(0, "end")
            for article in self.articles:
                self.article_list.insert("end", article.title)

            messagebox.showinfo("Success", f"Fetched {len(self.articles)} articles from {category}.")
        except Exception as e:
            messagebox.showerror("Error", f"Failed to fetch articles: {e}")

    def view_article(self):
        """View the selected article."""
        selected_index = self.article_list.curselection()
        if not selected_index:
            messagebox.showerror("Error", "No article selected.")
            return

        article = self.articles[selected_index[0]]
        messagebox.showinfo("Article", f"Title: {article.title}\n\n{article.summary}")

    def save_to_favorites(self):
        """Save the selected article to favorites."""
        selected_index = self.article_list.curselection()
        if not selected_index:
            messagebox.showerror("Error", "No article selected.")
            return

        article = self.articles[selected_index[0]]
        with open("favorites.txt", "a") as file:
            file.write(f"{article.title}\n{article.link}\n\n")

        messagebox.showinfo("Success", "Article saved to favorites.")


def main():
    root = Tk()
    app = NewsAggregator(root)
    root.mainloop()


if __name__ == "__main__":
    main()
command
C:\Users\Your Name\news-aggregator> python news_aggregator.py
# Click a category (Technology / Sports / World) to load its headlines.
# Select one and View Article, or Save to Favorites.

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid
news_aggregator.py
self.feeds = {
    "Technology": "https://rss.nytimes.com/services/xml/rss/nyt/Technology.xml",
    "Sports":     "https://rss.nytimes.com/services/xml/rss/nyt/Sports.xml",
    "World":      "https://rss.nytimes.com/services/xml/rss/nyt/World.xml",
}

Category → feed URL. Adding a source is a one-line edit. A button is generated per category in setup_ui.

news_aggregator.py
feed = feedparser.parse(feed_url)
self.articles = feed.entries
for article in self.articles:
    self.article_list.insert("end", article.title)

feedparser.parse handles the messy reality of RSS and Atom, returning a uniform object. feed.entries is the list of articles; each has .title, .summary, .link, and usually .published. This normalization is the whole reason to use the library.

news_aggregator.py
selected_index = self.article_list.curselection()
article = self.articles[selected_index[0]]
messagebox.showinfo("Article", f"Title: {article.title}\n\n{article.summary}")

curselection() returns the highlighted row’s index; you use it to look up the matching article object. This list-then-detail flow is how every email client and reader works.

news_aggregator.py
with open("favorites.txt", "a") as file:
    file.write(f"{article.title}\n{article.link}\n\n")

Appending to a text file is the simplest possible persistence. You’ll upgrade this to structured JSON below.

feedparser.parse makes a network call — on the main thread it freezes the whole window until the feed loads. Fetch in a background thread and update the UI safely:

threaded.py
import threading
 
def fetch_articles(self, category):
    threading.Thread(target=self._fetch, args=(category,), daemon=True).start()
 
def _fetch(self, category):
    feed = feedparser.parse(self.feeds[category])
    # schedule the UI update back on the main thread
    self.root.after(0, lambda: self._populate(feed.entries))

Network work off-thread, widget updates via root.after — the same rule as every Tkinter app.

The same wire story shows up under multiple sources. Dedupe by link:

dedupe.py
def merge(entry_lists):
    seen, merged = set(), []
    for entries in entry_lists:
        for e in entries:
            if e.link not in seen:
                seen.add(e.link)
                merged.append(e)
    return merged

Filter the visible list as the user types:

search.py
def on_search(self, query):
    q = query.lower()
    self.article_list.delete(0, "end")
    for a in self.articles:
        if q in a.title.lower() or q in a.summary.lower():
            self.article_list.insert("end", a.title)

Text files can’t store structure. Save full records:

favorites.py
import json
from pathlib import Path
 
def save_favorite(article):
    path = Path("favorites.json")
    favs = json.loads(path.read_text()) if path.exists() else []
    favs.append({"title": article.title, "link": article.link,
                 "summary": article.summary})
    path.write_text(json.dumps(favs, indent=2))
ProblemCauseFix
Window freezes on fetchNetwork call on the main threadFetch on a background thread + root.after
IndexError on View ArticleNo row selectedCheck curselection() before indexing
Same story listed twiceMultiple feeds, same articleDedupe by link
Garbled HTML in summaryRSS summaries contain HTMLStrip tags with html.parser or BeautifulSoup
Favorites lost on restartOnly kept in memoryPersist to JSON
Feed silently emptyBad URL or feed offlineCheck feed.bozo / feed.status and report it
  1. Add your own feeds — let users paste any RSS URL.
  2. Auto-refresh — re-fetch every N minutes with after.
  3. Open in browserwebbrowser.open(article.link) on double-click.
  4. Unread tracking — bold unread items, mark read on view.
  5. Offline reading — cache article text locally.
  6. Keyword alerts — notify when a story matches a watchlist.
  7. Sentiment tags — score headlines positive/negative with NLP.
  • News readers — Feedly, Inoreader, NetNewsWire.
  • Media monitoring — tracking mentions of a brand or topic.
  • Research dashboards — aggregating sources by subject.
  • Podcast clients — RSS is also how podcasts are distributed.
  • Working with feeds — RSS/Atom parsing and normalization.
  • GUI responsiveness — threading network I/O.
  • Data hygiene — deduplication and search.
  • Persistence — moving from flat text to structured JSON.
  • Move fetching to a background thread.
  • Dedupe across feeds and add search.
  • Upgrade favorites to JSON with full records.
  • Add auto-refresh and open-in-browser.

You built a news aggregator that unifies multiple RSS feeds into one browsable, searchable, savable front page — then fixed the freeze, the duplicates, and the fragile storage that hold back the naive version. The fetch → list → detail → persist loop you practiced is the skeleton of every reader, inbox, and feed app. Full source on GitHub. Explore more data and API projects on Python Central Hub.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading