Python Central HubSign in

Python Projects / Intermediate

News Aggregator

Build a Tkinter news aggregator in Python that pulls headlines from multiple RSS feeds, browse and read summaries, and save favorites — then add threaded fetching, search, deduplication, and persistent storage.

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

Prerequisites

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

Getting Started

Create the project

  1. Create a folder named news-aggregator.
  2. Inside it, create news_aggregator.py.
  3. Install the dependency: pip 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()

Run it

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

How it fits together

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

Step-by-Step Explanation

1. Feeds as a dictionary

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

2. Parsing a feed

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

3. The list → detail pattern

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

4. Saving favorites

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

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

Deduplicate Across Feeds

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

Python
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:

Python
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:

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

Variations to Try

  1. Add your own feeds — let users paste any RSS URL.
  2. Auto-refresh — re-fetch every N minutes with after.
  3. Open in browser — 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.

Checkpoint exercise

This checkpoint practices the aggregation step: parse an RSS feed from an inline string, drop duplicate links and list the newest headlines first.

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

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.

Was this page helpful?

Let us know how we did