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
feedparserfeedparsernormalizes 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
- Create a folder named
news-aggregatornews-aggregator. - Inside it, create
news_aggregator.pynews_aggregator.py. - Install the dependency:
pip install feedparserpip install feedparser.
Write the code
news_aggregator.py
Source"""
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
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
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.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
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.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
feed = feedparser.parse(feed_url)
self.articles = feed.entries
for article in self.articles:
self.article_list.insert("end", article.title)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
selected_index = self.article_list.curselection()
article = self.articles[selected_index[0]]
messagebox.showinfo("Article", f"Title: {article.title}\n\n{article.summary}")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
with open("favorites.txt", "a") as file:
file.write(f"{article.title}\n{article.link}\n\n")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:
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))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:
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 mergeddef 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 mergedAdd Search
Filter the visible list as the user types:
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)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:
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))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
| Problem | Cause | Fix |
|---|---|---|
| Window freezes on fetch | Network call on the main thread | Fetch on a background thread + root.afterroot.after |
IndexErrorIndexError on View Article | No row selected | Check curselection()curselection() before indexing |
| Same story listed twice | Multiple feeds, same article | Dedupe by linklink |
| Garbled HTML in summary | RSS summaries contain HTML | Strip tags with html.parserhtml.parser or BeautifulSoupBeautifulSoup |
| Favorites lost on restart | Only kept in memory | Persist to JSON |
| Feed silently empty | Bad URL or feed offline | Check feed.bozofeed.bozo / feed.statusfeed.status and report it |
Variations to Try
- Add your own feeds — let users paste any RSS URL.
- Auto-refresh — re-fetch every N minutes with
afterafter. - Open in browser —
webbrowser.open(article.link)webbrowser.open(article.link)on double-click. - Unread tracking — bold unread items, mark read on view.
- Offline reading — cache article text locally.
- Keyword alerts — notify when a story matches a watchlist.
- 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 coffeeWas this page helpful?
Let us know how we did
