RSS Feed Reader
Abstract
RSS (Really Simple Syndication) is the oldest still-working standard for publishing a list of articles in a machine-readable format. Most blogs, news sites, podcast hosts, and YouTube channels expose an RSS or Atom feed. In this project you will build a Python program that fetches a feed, parses it, and displays each article’s title, summary, publication date, author, and tags — then optionally opens any article in your default browser.
This project teaches:
- How HTTP and feed formats fit together.
- How to use the
feedparserfeedparserlibrary to handle the messy real-world variations of RSS and Atom. - How to extract structured data and present it nicely.
- How to extend a simple reader into a personalized news aggregator.
What Is RSS?
An RSS feed is a single XML document served from a URL. It contains:
- Feed-level metadata — title, description, link to the website, last-updated date.
- A list of entries — each one with its own title, link, summary or content, published date, author, and categories (tags).
When you “subscribe to” a feed, your reader app fetches that URL on a schedule and shows you new entries. There is no signup, no algorithm, no tracking — just a URL.
Feeds typically live at URLs like:
https://news.ycombinator.com/rsshttps://news.ycombinator.com/rsshttps://www.reddit.com/r/Python/.rsshttps://www.reddit.com/r/Python/.rsshttps://feeds.bbci.co.uk/news/rss.xmlhttps://feeds.bbci.co.uk/news/rss.xmlhttps://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_IDhttps://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- An internet connection.
- Comfort installing packages with
pippip.
Install Dependencies
pip install feedparserpip install feedparserfeedparserfeedparser is the standard Python library for reading both RSS and Atom (a slightly different, newer format). It hides the differences between formats so you only deal with a uniform structure.
Getting Started
Create the project
- Create a folder named
rss-feed-readerrss-feed-reader. - Inside it, create
rssfeedreader.pyrssfeedreader.py. - Open the folder in your editor.
Write the code
RSS Feed Reader
Source# RSS Feed Reader
import feedparser # pip install feedparser
import webbrowser
# RSS Feed URL
url = 'https://www.reddit.com/r/Python/.rss'
# Parse RSS Feed
feed = feedparser.parse(url)
# Print Feed Title
print(feed['feed']['title'])
# Print Entry Titles
for entry in feed['entries']:
print(entry['title'])
# Open Entry Link in Browser
webbrowser.open(entry['link'])
# Print Entry Summary
print(entry['summary'])
# Print Entry Date
print(entry['published'])
# Print Entry Author
print(entry['author'])
# Print Entry Tags
for tag in entry['tags']:
print(tag['term'])
# Print Entry ID
print(entry['id'])
# Print Entry Link
print(entry['link'])
# RSS Feed Reader
import feedparser # pip install feedparser
import webbrowser
# RSS Feed URL
url = 'https://www.reddit.com/r/Python/.rss'
# Parse RSS Feed
feed = feedparser.parse(url)
# Print Feed Title
print(feed['feed']['title'])
# Print Entry Titles
for entry in feed['entries']:
print(entry['title'])
# Open Entry Link in Browser
webbrowser.open(entry['link'])
# Print Entry Summary
print(entry['summary'])
# Print Entry Date
print(entry['published'])
# Print Entry Author
print(entry['author'])
# Print Entry Tags
for tag in entry['tags']:
print(tag['term'])
# Print Entry ID
print(entry['id'])
# Print Entry Link
print(entry['link'])
Run it
python rssfeedreader.pypython rssfeedreader.pySample output:
Feed Title: Python - Reddit
Article 1: How to build a web scraper with Python
Summary: Learn the basics of web scraping using BeautifulSoup...
Published: Mon, 01 Sep 2025 10:30:00 GMT
Author: pythondev123
Tags: programming, python, web-scrapingFeed Title: Python - Reddit
Article 1: How to build a web scraper with Python
Summary: Learn the basics of web scraping using BeautifulSoup...
Published: Mon, 01 Sep 2025 10:30:00 GMT
Author: pythondev123
Tags: programming, python, web-scrapingStep-by-Step Explanation
1. Import what you need
import feedparser
import webbrowserimport feedparser
import webbrowserfeedparserfeedparserdoes all the HTTP + XML work.webbrowserwebbrowseropens URLs in the user’s default browser with one call.
2. Fetch and parse the feed
url = "https://www.reddit.com/r/Python/.rss"
feed = feedparser.parse(url)url = "https://www.reddit.com/r/Python/.rss"
feed = feedparser.parse(url)A single call. Under the hood feedparserfeedparser:
- Sends an HTTP
GETGETto the URL. - Reads the response body.
- Parses it as XML.
- Normalizes the differences between RSS 1.0, RSS 2.0, and Atom into a common object structure.
The result is an object that looks like a dictionary. You can also access values with dot notation: feed.feed.titlefeed.feed.title.
3. Read feed-level metadata
print("Feed Title:", feed["feed"]["title"])
print("Feed Link:", feed["feed"]["link"])
print("Feed Description:", feed["feed"].get("description", "—"))print("Feed Title:", feed["feed"]["title"])
print("Feed Link:", feed["feed"]["link"])
print("Feed Description:", feed["feed"].get("description", "—"))Note feed["feed"]feed["feed"] — yes, the feed object has a feedfeed key for the feed-level fields, separate from the list of entries. It is a quirk of feedparserfeedparser’s API.
Using .get(key, default).get(key, default) avoids KeyErrorKeyError when a feed omits an optional field.
4. Loop through entries
for i, entry in enumerate(feed.entries, start=1):
print(f"\nArticle {i}: {entry.title}")
print(f"Summary: {entry.get('summary', '')}")
print(f"Published: {entry.get('published', 'unknown')}")
print(f"Author: {entry.get('author', 'unknown')}")
tags = [t.term for t in entry.get('tags', [])]
if tags:
print(f"Tags: {', '.join(tags)}")for i, entry in enumerate(feed.entries, start=1):
print(f"\nArticle {i}: {entry.title}")
print(f"Summary: {entry.get('summary', '')}")
print(f"Published: {entry.get('published', 'unknown')}")
print(f"Author: {entry.get('author', 'unknown')}")
tags = [t.term for t in entry.get('tags', [])]
if tags:
print(f"Tags: {', '.join(tags)}")enumerate(..., start=1)enumerate(..., start=1)gives you both the index (starting at 1) and the entry.- Each entry has at least
titletitleandlinklink. Everything else is optional and varies by source. - Tags arrive as a list of objects;
t.termt.termis the actual tag string.
5. Open articles in the browser
choice = input("\nEnter article number to open (or press Enter to skip): ")
if choice.strip().isdigit():
n = int(choice) - 1
if 0 <= n < len(feed.entries):
webbrowser.open(feed.entries[n].link)choice = input("\nEnter article number to open (or press Enter to skip): ")
if choice.strip().isdigit():
n = int(choice) - 1
if 0 <= n < len(feed.entries):
webbrowser.open(feed.entries[n].link)webbrowser.open(url)webbrowser.open(url) launches your default browser pointed at that URL. Cross-platform with no setup.
Cleaning Up HTML in Summaries
Many feeds put HTML in the summary field — paragraphs, links, embedded images. To show plain text, strip the tags:
import re
def strip_html(text):
return re.sub(r"<[^>]+>", "", text or "").strip()
print(strip_html(entry.summary))import re
def strip_html(text):
return re.sub(r"<[^>]+>", "", text or "").strip()
print(strip_html(entry.summary))For higher-quality output, use BeautifulSoup:
from bs4 import BeautifulSoup
text = BeautifulSoup(entry.summary, "html.parser").get_text()from bs4 import BeautifulSoup
text = BeautifulSoup(entry.summary, "html.parser").get_text()Handling Network Errors
feedparserfeedparser does not raise on bad URLs; it sets a status. Always check it:
feed = feedparser.parse(url)
if feed.bozo: # bozo flag = something went wrong
print(f"Warning: feed may be malformed — {feed.bozo_exception}")
if not feed.entries:
print("No entries found.")feed = feedparser.parse(url)
if feed.bozo: # bozo flag = something went wrong
print(f"Warning: feed may be malformed — {feed.bozo_exception}")
if not feed.entries:
print("No entries found.")Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
KeyError: 'author'KeyError: 'author' | Feed omits an optional field | Use entry.get('author', 'unknown')entry.get('author', 'unknown') |
UnicodeEncodeErrorUnicodeEncodeError printing entries | Terminal cannot render some characters | Set PYTHONIOENCODING=utf-8PYTHONIOENCODING=utf-8 or pipe to a file |
Empty feed.entriesfeed.entries | URL is HTML, not a feed; or site blocks the user agent | Pass a custom User-AgentUser-Agent via feedparser.parse(url, request_headers={...})feedparser.parse(url, request_headers={...}) |
webbrowser.openwebbrowser.open does nothing | Running in a headless environment | Print the URL instead |
Variations to Try
1. Read multiple feeds
FEEDS = [
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/Python/.rss",
"https://feeds.bbci.co.uk/news/technology/rss.xml",
]
for url in FEEDS:
feed = feedparser.parse(url)
print(f"\n=== {feed.feed.title} ===")
for entry in feed.entries[:5]:
print(f" • {entry.title}")FEEDS = [
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/Python/.rss",
"https://feeds.bbci.co.uk/news/technology/rss.xml",
]
for url in FEEDS:
feed = feedparser.parse(url)
print(f"\n=== {feed.feed.title} ===")
for entry in feed.entries[:5]:
print(f" • {entry.title}")2. Sort entries by date
Use the parsed published_parsedpublished_parsed field, which is a Python struct_timestruct_time:
import time
entries = sorted(feed.entries,
key=lambda e: e.get("published_parsed", time.gmtime(0)),
reverse=True)import time
entries = sorted(feed.entries,
key=lambda e: e.get("published_parsed", time.gmtime(0)),
reverse=True)3. Save articles offline
Write each entry’s text content to a file for offline reading:
from pathlib import Path, PurePath
safe = "".join(c for c in entry.title if c.isalnum() or c == " ")[:80]
Path(f"articles/{safe}.txt").write_text(entry.summary, encoding="utf-8")from pathlib import Path, PurePath
safe = "".join(c for c in entry.title if c.isalnum() or c == " ")[:80]
Path(f"articles/{safe}.txt").write_text(entry.summary, encoding="utf-8")4. Show only new articles (since last run)
Store the timestamp of the last fetch in a file. Filter entries newer than that:
last_run = float(Path("last_run.txt").read_text() or 0)
new_entries = [e for e in feed.entries
if time.mktime(e.published_parsed) > last_run]
Path("last_run.txt").write_text(str(time.time()))last_run = float(Path("last_run.txt").read_text() or 0)
new_entries = [e for e in feed.entries
if time.mktime(e.published_parsed) > last_run]
Path("last_run.txt").write_text(str(time.time()))5. Send a daily email digest
Combine feedparserfeedparser with smtplibsmtplib (Python’s email module) to mail yourself the new entries each morning.
6. Build a GUI reader
Wrap the loop in a Tkinter app with a feed list on the left and articles on the right. See Simple Blog System for the layout pattern.
7. Convert to OPML
OPML is the standard format readers use to import/export feed subscriptions. Generate one from your list of URLs.
8. Search across feeds
After fetching, store all entries in a list and let the user filter by keyword:
keyword = input("Search: ").lower()
matches = [e for e in all_entries if keyword in e.title.lower()]keyword = input("Search: ").lower()
matches = [e for e in all_entries if keyword in e.title.lower()]Best Practices
- Cache responses. Hitting the same feed every few seconds is rude. Honor the feed’s
etagetagandmodifiedmodifiedheaders and pass them on subsequent requests so the server can answer with304 Not Modified304 Not Modified.cache.pyfeed = feedparser.parse(url, etag=last_etag, modified=last_modified)cache.pyfeed = feedparser.parse(url, etag=last_etag, modified=last_modified) - Set a polite User-Agent identifying your app and a contact URL.
- Limit display length. Truncate long summaries to keep the terminal readable.
- Validate input. When letting the user open entry N, check N is in range.
Real-World Applications
- Personal news aggregators (think Feedly, NetNewsWire).
- Podcast catchers — podcasts publish RSS feeds with
<enclosure><enclosure>tags pointing to MP3s. - Monitoring tools — turn build-failure feeds, CVE feeds, or pricing feeds into alerts.
- Content backups — archive a blog by downloading every entry.
- Cross-posting pipelines — read from one platform’s feed, post to another.
Educational Value
This project teaches:
- Working with web data — HTTP requests are hidden but real.
- Structured data extraction — feeds, JSON APIs, scraped pages all share the same patterns.
- Defensive coding —
entry.get(key, default)entry.get(key, default)is the difference between robust and brittle. - Module ecosystem — when to reach for a library (
feedparserfeedparser) instead of parsing XML by hand. - User experience — even a CLI tool benefits from clean formatting and gentle error messages.
Next Steps
- Build a subscription manager that stores feed URLs in a JSON file and lets the user add/remove them.
- Add a search index with
sqlite3sqlite3so you can grep entries across hundreds of feeds. - Turn it into a web service with Flask (see Basic Web Server) that shows your feeds on a web page.
- Bridge feeds to Discord or Slack with their webhook APIs.
- Plug into a TTS engine (
pyttsx3pyttsx3) for an audio news briefing.
Conclusion
A few dozen lines of Python plus feedparserfeedparser is enough to replicate the core of a commercial RSS reader. You learned to fetch a feed, walk its entries, handle missing fields gracefully, and open articles in the browser. The same patterns will serve you anywhere you consume structured data from the web. The full source is on GitHub. Explore more 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
