reader: a fat model for building your own feed reader
A Python feed reader library.
At a glance
- What is it?
- reader is a Python library that handles storing, updating, searching and tagging Atom, RSS and JSON feeds, and deliberately ships no web framework and no opinion about how you use it. The storage, the feed parsing, the full text search and the entry state all live in the library, so the application you write is only the part that makes your reader different.
- Who is it for?
- Adopt reader if you are writing a feed reader, a podcast client or an internal aggregator and want the parts that are hard, feed parsing, entry state, tag filtering, OPML import and export, feed discovery and full text search, already solved in a typed library. Do not adopt it if you want a ready made reader to install, since the web application and the command line interface are both described as optional extras rather than the product.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A fat model, and what that means for the code you write
The README describes reader as a Python feed reader library, and the design intent in one line: it is designed to allow writing feed reader applications without any business code, and without depending on a particular framework. In MVC terms, the README says you would probably call it a fat model.
That framing is the most useful thing in the documentation, because it sets the boundary of the library precisely. The heavy part of a feed reader, the part that is tedious and easy to get subtly wrong, is the data: parsing feeds of several formats, keeping entry state across updates, deciding what is new, filtering by tag, indexing article text for search, importing and exporting OPML, and discovering feeds buried in HTML pages. All of that is in the library.
What is not in the library is a presentation layer and a policy layer. The README is explicit that reader does not depend on a web framework and does not have an opinion of how or where you use it. So the application you write is thin by design, and the README's own summary of the goal is to skip all the low level stuff and focus on what makes your feed reader different.
The audience follows from that. This is a library for people who are building a reader rather than running one, whether that is a self-hosted service, a research tool that pulls a hundred feeds for analysis, or a personal reader with a UI nobody else has.
The project topics tell you the format coverage, and they are wide: atom-feed, rss, json-feed, opml, podcast, feed-aggregator, podcatcher, and twitter, which is a legacy of the format support but a reminder that the parser handles more than the current web feeds. The keywords in the package metadata name the same set, plus cdf and rdf, which are the older feed formats.
The supporting claims are worth reading as commitments rather than as decoration. The README promises a stable, clearly documented API, excellent test coverage and fully typed Python, and the repository backs that with a codecov badge, a mypy badge, a black code style badge and a build badge.
Installing it, and the first five minutes
The package is on PyPI as reader, and installation is a single command:
$ pip install readerThe usage section of the README is a doctest style session, which is a good sign, because it means the documentation is executable and the examples are checked rather than written by hand. The first steps are to create a reader over a SQLite file, add a feed, update the feeds, and read the entries back:
>>> from reader import make_reader
>>>
>>> reader = make_reader('db.sqlite')
>>> reader.add_feed('http://www.hellointernet.fm/podcast?format=rss')
>>> reader.update_feeds()
>>>
>>> entries = list(reader.get_entries())
>>> [e.title for e in entries]
['H.I. #108: Project Cyclops', 'H.I. #107: One Year of Weird', ...]
>>>
>>> reader.mark_entry_as_read(entries[0])The single argument to make_reader is a path to a database file, and the library creates it. The example uses a podcast feed served with an RSS format, which is worth noting because the podcast case is where the interesting edge cases live: enclosures, durations, and the fact that the same feed format is used for audio.
The state operations are then plain method calls on the reader object. mark_entry_as_read changes one entry, and the entries can be queried by state, since the README goes on to call get_entries with read=False and then with read=True to show the two sides of the partition.
The dependency list explains the choices, and it is short. feedparser at version 6 or later does the actual feed parsing, requests at 2.18 or later does the HTTP, werkzeug above version 2 is there for the HTTP utilities, beautifulsoup4 at 4.5 or later handles the HTML for feed discovery and for stripping article content, structlog does the logging, and typing-extensions at version 4 or later backfills the typing features on newer Python versions.
The interesting design choice is that feed parsing is delegated to feedparser rather than reimplemented. That is a sensible division of labour, and it also means the library inherits feedparser's behaviour on malformed feeds, which is generally good and occasionally surprising.
The entry lifecycle: read, important, tags and metadata
The feature list is the library's API surface in brief. reader allows you to retrieve, store and manage Atom, RSS and JSON feeds, mark articles as read or important, add arbitrary tags and metadata to feeds and articles, filter feeds and articles, do full text search on articles, get statistics on feed and user activity, import and export feeds as OPML, automatically discover feeds in web pages, write plugins to extend the functionality, and skip the low level work.
The detail that matters most for a reader application is that tags and metadata are arbitrary. Not a fixed set of statuses but user-defined tags on both feeds and articles, with the filter and query methods built on top of them. A reader that wants starred, archived and later-read is expressing three tags rather than three columns, and one that wants something else can add it without a schema change.
The read and important markers sit alongside that as first class state, because those are the two things every reader needs and no application should be inventing.
Statistics on feed and user activity are also in the library, which is the kind of feature that usually gets written twice in every project that needs it, once badly. OPML import and export is in there too, and it is the format people actually have when they arrive at a new reader, so supporting it is the difference between a working migration path and a support ticket.
The entry objects returned by the queries are the interface between the library and the application. In the documented example the title is read as e.title directly, while the search result reads it through e.metadata.get('.title'), and that difference is informative: the metadata mapping holds the parsed feed fields under their canonical names, and a value object exposes the text plus, for search results, the highlight positions. The metadata approach is what lets a library support many feed formats without a per-format object model.
The underlying storage is a SQLite file passed to make_reader, with schema migrations handled by the library as the version moves, so a long-lived installation can be upgraded without the application knowing anything about it.
Full text search, and the shape of its results
Full text search is a feature most feed libraries leave to the application, and the way reader does it is the one part of the API worth reading closely.
Search is a separate operation from updating the feeds. The documented sequence calls reader.update_search() before searching, which tells you the index is not maintained as a side effect of fetching. That is a deliberate design: updating and indexing are different costs, and a reader that only reads headlines should not pay for building a search index.
The query is then a method with a text argument, a limit and, in the example, a limit of three:
>>> reader.update_search()
>>>
>>> for e in reader.search_entries('year', limit=3):
... title = e.metadata.get('.title')
... print(title.value, title.highlights)
...
H.I. #107: One Year of Weird (slice(15, 19, None),)
H.I. #52: 20,000 Years of Torment (slice(17, 22, None),)
H.I. #83: The Best Kind of Prison ()The output is the thing to notice. The highlights are slice objects, slices of 15 to 19, 17 to 22, and so on, not extracted text fragments. A slice points at where the term appears in the value, so the application decides how to render a highlight, whether that is wrapping the matched range in a tag, showing an excerpt around it, or ignoring it.
The third result shows an empty tuple of highlights, which reads oddly until you realise that a hit can be on another field of the entry rather than on the title, in which case the title has no highlight to report. That is a design choice rather than a bug, and it is the kind of thing a user interface has to handle.
The cost of the approach is that the library hands you positions rather than a snippet, so the interface work is on you. The benefit is that the search layer stays format agnostic, because it operates on the parsed metadata rather than on a rendered HTML article.
Plugins, feed discovery, and the examples that show the seams
Three capabilities sit outside the core CRUD path and are worth describing separately, because they are the ones that determine whether a reader can grow.
Feed discovery automatically finds feeds in web pages. That is the feature that lets a user paste a blog URL rather than hunt for the RSS link, and it is implemented on top of the HTML parsing dependency rather than by scraping heuristics of its own. The cost is that it depends on pages still marking up their feeds, and the benefit is that a reader which does not implement this is a worse product than one that does.
Plugins are the extension mechanism for functionality that does not belong in the core. The README lists writing plugins to extend the functionality as a first class capability, and the plugin system is what a project like this needs in order not to accumulate every feature in the main code path.
The examples directory is the best guide to the library's seams, and the file names describe them. There is a config.toml, which points at the configuration surface. There is custom_headers.py, which shows how to add request headers, and that matters for any feed behind an authorisation header or a user agent policy. There is feed_slugs.py, which shows the identifier scheme, since a reader needs stable, URL independent handles for feeds that change address. There is parser_only.py, which is the most interesting one for anyone who does not want a database at all: it shows parsing a feed without storing it, for a one off script that wants an article's text and nothing else. There is podcast.py for the podcast specifics, and terminal.py for a terminal interface.
Those two together, parser_only and terminal, are worth pairing, because they bracket the library's range. The same code that can be a full reader with a database and a web interface can also be a script that parses one feed and prints one article, without committing to the storage model at all.
The optional web app, the optional CLI, and the framework boundary
Beyond the library, reader provides an optional web application based on Flask, Bootstrap and htmx, and an optional command line interface. Both are described as optional extras, and that word choice is the important part of the design.
The web application is deliberately minimal in its dependencies. Flask for the server, Bootstrap for the styling and htmx for the interactions, which means the client side is not a JavaScript application with a build step, and the server renders the pages. For a reader whose pages are lists of articles and a few forms, that is a proportionate choice, and it also means the optional app is not an opinionated front end that fights your own.
The command line interface is the other optional extra, and it is a reasonable smoke test for the library: if the CLI can list and update feeds, the core works.
Both extras are optional in the sense that they are separate from the importable library, not that they are separate packages you have to install, since the repository is a single distribution. The practical consequence is that adding reader to a project gives you the core and the extras together, and the extras are useful but not what you are adopting the library for.
The boundary the README draws is the one to hold on to. reader does not depend on a web framework, so your application is free to be a Django app, a FastAPI service, a Streamlit script, a terminal tool, or a scheduled job. What it does not do is offer an opinion about how or where you use it, which cuts both ways: you get freedom, and you get the work of choosing, and of keeping the entry state model consistent with whatever your interface needs to show.
For a team that already has a web stack, that is exactly right. For someone who wants a working reader to install tonight, the optional web app is the answer, and the fact that it is a thin Flask and htmx application rather than a full product is a reasonable expectation to set.
BSD-3-Clause, Python 3.12 and a steady release line
The licence is BSD-3-Clause, with the LICENSE file at the root of the repository, which is a permissive licence with no copyleft obligation for a service you run or a product you ship.
The Python requirement is specific. pyproject.toml sets requires-python to >=3.12, and the classifiers list Python 3.12, 3.13, 3.14 and 3.15, with CPython and PyPy both supported and a Typing :: Typed classifier. A project tracking four Python versions and two implementations is a project that takes its typing seriously, and the mypy badge in the README is the corresponding claim.
The release cadence is regular. The recent versions are 3.27 from 2026-10-11, 3.26 from 2026-06-24 and 3.25 from 2026-05-25, and the last push to the repository was on 2026-10-15. A patch-level number in the twenties on a 3.x version is the shape of a library that has been shipping small releases for a while, and the changelog lives in CHANGES.rst in the repository with a rendered version on the documentation site.
The supporting infrastructure in the tree is what you would expect from a project that claims excellent test coverage. There is a tests directory, a tox.ini for running the suite across environments, a .flake8 configuration, a .pre-commit-config.yaml, a .readthedocs.yaml for the documentation build, a run.sh convenience script and a scripts directory. The documentation itself is on readthedocs with a status badge, and the repository links a changelog page, an issues page and the project URLs section in the metadata.
The upgrade story for an application is therefore ordinary: pin a version, read CHANGES.rst before bumping, and expect the storage layer to handle the database migration for you, since the database file is an artefact the library owns rather than one your application manages.
Editorial conclusion
Adopt reader if you are writing a feed reader, a podcast client or an internal aggregator and want the parts that are hard, feed parsing, entry state, tag filtering, OPML import and export, feed discovery and full text search, already solved in a typed library. Do not adopt it if you want a ready made reader to install, since the web application and the command line interface are both described as optional extras rather than the product. Verify first that the search behaviour matches what you need, because the highlight values in the documented example are slices into the text rather than extracted snippets, that your feeds are of the kinds it parses, Atom, RSS and JSON, and that Python 3.12 or later is available to you, since pyproject.toml requires it and the classifiers list 3.12 through 3.15.
Frequently asked questions
What is the reader Python library?
reader is a Python feed reader library designed to let you write feed reader applications without any business code and without depending on a particular framework, which the README describes as a fat model in MVC terms. It handles retrieving, storing and managing Atom, RSS and JSON feeds.
How do I install reader and add a feed?
Run pip install reader, then create a reader over a SQLite file with make_reader('db.sqlite'), add a feed URL with add_feed, and call update_feeds() to fetch it. Entries are then read back with get_entries and their state changed with methods such as mark_entry_as_read.
How does full text search work in reader?
Search is a separate step from feed updating, so you call update_search() before searching. search_entries returns matching entries whose matched values expose a highlights attribute containing slices of the value rather than extracted snippets, and the application decides how to render them.
Does reader include a web interface or a command line tool?
Both are provided as optional extras: a web application built on Flask, Bootstrap and htmx, and a command line interface. The library itself does not depend on a web framework and does not have an opinion about how or where you use it.
What Python versions and licence apply to reader?
The package requires Python 3.12 or later, with classifiers covering 3.12 through 3.15 on both CPython and PyPy, and it is distributed under the BSD-3-Clause licence with the LICENSE file in the repository root.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/lemon24-reader)Was this useful?