News Aggregator
Abstract
Section titled “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
feedparsernormalizes 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
Section titled “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
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create a folder named
news-aggregator. - Inside it, create
news_aggregator.py. - Install the dependency:
pip install feedparser.
Write the code
Section titled “Write the code”news_aggregator.py
pch.viewSource"""
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
Section titled “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.How it fits together
Section titled “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.
flowchart TD RUN(["python news_aggregator.py"]) NewsAggregator["NewsAggregator
class"] main("main") RUN --> main main --> NewsAggregator
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. Feeds as a dictionary
Section titled “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",
}Category → feed URL. Adding a source is a one-line edit. A button is generated per category in setup_ui.
2. Parsing a feed
Section titled “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)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
Section titled “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}")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
Section titled “4. Saving favorites”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
Section titled “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:
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
Section titled “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 mergedAdd Search
Section titled “Add 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)Upgrade Favorites to JSON
Section titled “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))Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| Window freezes on fetch | Network call on the main thread | Fetch on a background thread + root.after |
IndexError on View Article | No row selected | Check curselection() before indexing |
| Same story listed twice | Multiple feeds, same article | Dedupe by link |
| Garbled HTML in summary | RSS summaries contain HTML | Strip tags with html.parser or BeautifulSoup |
| Favorites lost on restart | Only kept in memory | Persist to JSON |
| Feed silently empty | Bad URL or feed offline | Check feed.bozo / feed.status and report it |
Variations to Try
Section titled “Variations to Try”- Add your own feeds — let users paste any RSS URL.
- Auto-refresh — re-fetch every N minutes with
after. - Open in browser —
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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading