Chapter 20 · Content types, static files and the other contrib apps
20.12 Feeds: django.contrib.syndication and the feed generator
How a feed, a subclass of django, answers a request with an RSS or Atom document: the object it is about, the attributes that may each be a value or a method, and the feed generator of django/, a SyndicationFeed such as Rss201rev2Feed or Atom1Feed, which writes the XML.
A feed is made in two layers. The view knows the request: it finds the object, takes the current site’s domain for links, renders templates and makes dates aware. The feed generator knows nothing of requests or models: it keeps what it is handed in dictionaries and writes one dialect of XML.
A feed is a view
The URLconf holds one instance of a Feed subclass, and every request to its pattern calls that one object (What the handler asks of a view). Feed.__call__ does four things:
- It passes the request and the captured arguments to
Feed.get_object, which by default returnsNone. AnObjectDoesNotExistfrom it, such as aModelor a.DoesNotExist FeedDoesNotExist, becomesHttp404("Feed object does not exist."). - It calls
Feed.get_feedfor a filled feed generator. - It makes an
HttpResponsewith the generator’scontent_type, and, if the feed has an attribute"item_pubdate"or"item_updateddate", a Last-Modified header fromSyndicationFeed, the newest date of any item or else the present, so that, says the comment,.latest_post_date ConditionalGetMiddlewarecan answer 304 (Cache headers and conditional requests:django/).utils/ cache .py - It has the generator write itself into the response, in UTF-8.
The office’s second feed is of one port’s bulletins, in Atom, a chart attached to one; port 9, which does not exist, gave a 404:
# notes/feeds.py, in part
from django.utils.feedgenerator import Atom1Feed
class PortBulletins(Feed):
feed_type = Atom1Feed
link = "/bulletins/"
def get_object(self, request, pk):
return Port.objects.get(pk=pk)
def title(self, obj):
return f"Bulletins for {obj.name}"
def items(self, obj):
return obj.bulletins.order_by("-published")
def item_pubdate(self, item):
return item.published
def item_enclosure_url(self, item):
if item.title == "Breakwater closed":
return "/media/charts/north-breakwater.pdf"
item_enclosure_length = 48213
item_enclosure_mime_type = "application/pdf"
# in the URLconf
path("ports/<int:pk>/bulletins.atom", PortBulletins()),
A value or a method: _get_dynamic_attr
Every part of a feed but four plain attributes, Feed.feed_type, Feed.language, Feed and Feed, and two methods always called, Feed and Feed, is read through Feed, with one argument: the object from Feed.get_object for a part of the feed, the item for a part of an item. A missing name gives a default, None unless the caller passes one; a value that is not callable is the answer. A callable is called with the argument if its code has two positional parameters, self and one more, and otherwise without. The code is reached through inspect.unwrap, which follows the __wrapped__ that functools.wraps leaves on a decorator’s function, or, for an object with no __code__, through its __call__. The comment prefers this to catching a TypeError from the call, since one might come from inside the function.
Code with no positional parameter, a wrapper of *args, is taken for a decorator made without functools.wraps and refused instead, unless the feed holds a staticmethod under the function’s own name. An "item_title" decorated so raised ImproperlyConfigured: “Feed method ‘item_title’ decorated by ‘wrapper’ needs to use @functools.wraps.”
Filling the generator: get_feed
Feed.get_feed works in this order:
- It asks
get_current_sitefor the current site, aSite, or aRequestSitewhen the sites application is not installed (Sites:Site,get_current_siteand theshortcutview). - It makes the generator, an instance of
Feed.feed_type, by defaultRss201rev2Feed, from"title","link","description"and the rest, with the request’s path if there is no"feed_url",djangoif there is no.utils .translation .get_language Feed.language, andFeed..feed_extra_kwargs - It loads
Feedand.title_template Feed; a missing one is passed over silently..description_template - For each of
"items", it renders those templates with the context ofFeed, the item as.get_context_data "obj"and the site as"site", or else asks"item_title"and"item_description", by default the item’sstr(for the title, escaped). The link is"item_link", by default the item’sget_absolute_url. A naive"item_pubdate"or"item_updateddate"is made aware in the default time zone, whateverUSE_TZsays (Time zones);"item_guid"defaults to the link.SyndicationFeedtakes these and.add_item Feed..item_extra_kwargs
The feed’s link, its "feed_url" and each item’s link go through add_domain: a path gets the scheme, "https" when HttpRequest says so, and the domain; a URL beginning "http://", "https://" or "mailto:" is left alone, and one beginning "//" gets the scheme, which RSS requires, says the comment, citing ticket 16753. Domain and path are joined as they are: "bulletins/" became "http://. An enclosure’s URL is no link: Feed makes at most one Enclosure, from "item_enclosure_url", "item_enclosure_length" and "item_enclosure_mime_type", each through str, so a missing length is written "None".
Valletta’s feed, one element to a line, its first entry left out:
200 application/atom+xml; charset=utf-8
Last-Modified: Tue, 13 Oct 2026 07:30:00 GMT
<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom">
<title>Bulletins for Valletta</title>
<link href="http://harbour.example/bulletins/" rel="alternate"/>
<link href="http://harbour.example/ports/1/bulletins.atom" rel="self"/>
<id>http://harbour.example/bulletins/</id>
<updated>2026-10-13T07:30:00+00:00</updated>
...
<entry>
<title>Breakwater closed</title>
<link href="http://harbour.example/bulletins/1/" rel="alternate"/>
<published>2026-10-12T06:00:00+00:00</published>
<id>http://harbour.example/bulletins/1/</id>
<summary type="html">Breakwater closed</summary>
<link href="/media/charts/north-breakwater.pdf" length="48213" rel="enclosure" type="application/pdf"/>
</entry>
</feed>
The ids are the links. The times are in UTC, as the database returned them; the feed’s updated is the newest.
Feed.item_title escapes the item’s str as HTML, and its comment says titles should be double escaped by default, citing ticket 6533; the generator escapes again, as XML. A bulletin “Tugs & pilots” became <title>Tugs &amp; pilots</ in this feed, and <title>Tugs & pilots</ in the bulletins’ feed, whose "item_title" returns the title unescaped.
The feed generator: SyndicationFeed
SyndicationFeed keeps the feed’s values in a dictionary, text made strings and URLs escaped, the id "feed_guid" or else the link, and any keyword it does not know; SyndicationFeed appends one for each item. SyndicationFeed raises NotImplementedError: writing is a dialect’s.
RssFeed.write makes a SimplerXMLGenerator over the file (Text helpers: Truncator and django/) and writes the declaration, any stylesheets, an rss element and a channel, the feed’s elements and the items; Atom1Feed.write, a feed element and entries. Both call four hooks, empty on SyndicationFeed: SyndicationFeed, SyndicationFeed, SyndicationFeed and SyndicationFeed. A format built on RSS or Atom subclasses Rss201rev2Feed or Atom1Feed and extends them, calling the superclass’s, says the documentation. GeoDjango’s Feed uses one, GeoRSSFeed, handing it geometries through its own feed_extra_kwargs and item_extra_kwargs (GeoDjango’s forms, admin, feeds and data tools).
django/utils/feedgenerator.py . DefaultFeed is Rss201rev2Feed.| generator | content_type |
root | an item’s elements | enclosures |
|---|---|---|---|---|
Rss |
content_type "application/ |
root rss 0.91 | an item’s elements title, link, description | enclosures none |
Rss201rev2Feed, DefaultFeed |
content_type "application/ |
root rss 2.0 | an item’s elements those, author or dc:creator, pubDate, comments, guid, ttl, category | enclosures one; a second raises ValueError |
Atom1Feed |
content_type "application/ |
root feed | an item’s elements title, link, published, updated, author, id, summary, category, rights | enclosures each, as a link |
RSS writes dates with rfc2822_date and Atom with rfc3339_date; the channel’s lastBuildDate and the feed’s updated are SyndicationFeed. An Atom entry with no unique id gets one from get_tag_uri; from a feed view, only when "item_guid" returns None, as in "tag:.
Stylesheets
Each is a URL, made a Stylesheet, or a Stylesheet, whose type is guessed from the extension unless given, whose media is "screen" unless given, and whose URL is read only when written, so a lazy one from reverse_lazy works. A string alone raised TypeError: “stylesheets should be a list, not <class ‘str’>”. RssFeed writes them before the first tag. Atom1Feed.write never calls the hook, empty on SyndicationFeed, so the port’s feed, given the list, wrote none. The bulletins’ RSS feed given ["/ began:
<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/static/notes/feed.xsl" media="screen" type="text/xsl"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel>