Skip to content

News Aggregator

Abstract

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 feedparserfeedparser, 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 feedparserfeedparser 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.

Prerequisites

  • Python 3.6 or above.
  • A text editor or IDE.
  • feedparserfeedparser: pip install feedparserpip install feedparser.
  • Tkinter (bundled with Python).
  • Basic understanding of dictionaries and lists.

Getting Started

Create the project

  1. Create a folder named news-aggregatornews-aggregator.
  2. Inside it, create news_aggregator.pynews_aggregator.py.
  3. Install the dependency: pip install feedparserpip install feedparser.

Write the code

news_aggregator.pySource
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()
 
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()
 

Run it

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.
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.

Step-by-Step Explanation

1. Feeds as a dictionary

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",
}
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_uisetup_ui.

2. Parsing a feed

news_aggregator.py
feed = feedparser.parse(feed_url)
self.articles = feed.entries
for article in self.articles:
    self.article_list.insert("end", article.title)
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.parsefeedparser.parse handles the messy reality of RSS and Atom, returning a uniform object. feed.entriesfeed.entries is the list of articles; each has .title.title, .summary.summary, .link.link, and usually .published.published. This normalization is the whole reason to use the library.

3. The list → detail pattern

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}")
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()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.

4. Saving favorites

news_aggregator.py
with open("favorites.txt", "a") as file:
    file.write(f"{article.title}\n{article.link}\n\n")
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.

Fix: Don’t Freeze the UI

feedparser.parsefeedparser.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))
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.afterroot.after — the same rule as every Tkinter app.

Deduplicate Across Feeds

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
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)
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)

Upgrade Favorites to JSON

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))
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))

Common Mistakes

ProblemCauseFix
Window freezes on fetchNetwork call on the main threadFetch on a background thread + root.afterroot.after
IndexErrorIndexError on View ArticleNo row selectedCheck curselection()curselection() before indexing
Same story listed twiceMultiple feeds, same articleDedupe by linklink
Garbled HTML in summaryRSS summaries contain HTMLStrip tags with html.parserhtml.parser or BeautifulSoupBeautifulSoup
Favorites lost on restartOnly kept in memoryPersist to JSON
Feed silently emptyBad URL or feed offlineCheck feed.bozofeed.bozo / feed.statusfeed.status and report it

Variations to Try

  1. Add your own feeds — let users paste any RSS URL.
  2. Auto-refresh — re-fetch every N minutes with afterafter.
  3. Open in browserwebbrowser.open(article.link)webbrowser.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.

Real-World Applications

  • 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.

Educational Value

  • 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.

Next Steps

  • 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.

Conclusion

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.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did