DEV Community

Yanic K
Yanic K

Posted on

Parsing EPG/XMLTV TV Schedules in Python: Build a Smart Programme Guide (September 2026)

A dependency-free Python toolkit for parsing XMLTV/EPG television schedules — filter by channel and time window, regex-search titles, detect overlapping broadcasts, and export normalized JSON.

Disclosure: This technical/educational article may include reference or affiliate links. We only link to verified services and official platforms tested for performance and security.

Every streaming setup eventually hits the same wall: the guide. Players can open streams, but knowing what is on right now, and what airs next, requires EPG (Electronic Programme Guide) data — and almost all of it moves around in one format: XMLTV.

In this tutorial, we build a smart programme-guide layer in pure Python: parse an XMLTV document into typed dataclasses, filter programmes by channel and time window, search titles with regular expressions, detect overlapping broadcasts, and export a normalized JSON schedule — using the new tvnow-epg package, which runs on the standard library alone (Python 3.9+, zero dependencies).

📺 What EPG/XMLTV Data Actually Is

An Electronic Programme Guide is a structured listing of which programmes air on which channels at which times. XMLTV is the de-facto XML interchange format for these listings. A minimal document looks like this:

<tv>
  <channel id="example.news">
    <display-name>Example News HD</display-name>
  </channel>
  <programme start="20240610060000 +0000" stop="20240610090000 +0000"
             channel="example.news">
    <title>Morning News Desk</title>
    <category>News</category>
  </programme>
</tv>
Enter fullscreen mode Exit fullscreen mode

Two things make real-world EPG files messy rather than fun:

  1. Timestamps arrive as YYYYMMDDHHMMSS +ZZZZ strings with (sometimes) explicit UTC offsets.
  2. Malformed entries — channels without ids, programmes missing start times — are routine in guides scraped from broadcasters.

A production parser must skip broken entries instead of crashing, and treat naive timestamps deterministically. That is exactly the contract tvnow-epg implements.

💻 Installation & Quick Start

pip install tvnow-epg
Enter fullscreen mode Exit fullscreen mode
from tvnow_epg import parse_xmltv_file, filter_by_channel, to_json

schedule = parse_xmltv_file("guide.xml")

for channel in schedule.channels:
    print(channel.channel_id, "-", channel.display_name)

programmes = filter_by_channel(schedule.programmes, "example.news")
for p in programmes:
    print(p.start, "-", p.title)

print(to_json(schedule, channel_id="example.news"))
Enter fullscreen mode Exit fullscreen mode

parse_xmltv_file() returns a Schedule holding .channels (list of Channel) and .programmes (list of Programme, conveniently sorted by channel then start time).

🔍 Searching and Filtering Programmes

The two filters you will use most:

from tvnow_epg import filter_by_channel, search_titles

# All programmes on one channel
news = filter_by_channel(schedule.programmes, "example.news")

# Case-insensitive substring search over titles
matches = search_titles(schedule.programmes, "ocean")

# Regular-expression search
import re
matches = search_titles(schedule.programmes, r"^The\b", regex=True)
Enter fullscreen mode Exit fullscreen mode

The regex mode is more useful than it looks: r"^The\b" pulls every title starting with "The", and patterns like r"(?i)(premiere|finale)" surface season events a plain substring search would scatter across casing variants.

⏰ Time-Window Filtering That Behaves

EPG tooling usually wants "what's airing in this window" — for example, prime time:

from datetime import datetime, timezone
from tvnow_epg import filter_by_time

start = datetime(2026, 9, 22, 19, 0, tzinfo=timezone.utc)
stop  = datetime(2026, 9, 22, 23, 0, tzinfo=timezone.utc)

window = filter_by_time(schedule.programmes, start=start, stop=stop)
Enter fullscreen mode Exit fullscreen mode

A programme is included when its air time intersects the [start, stop) window — so a film that started at 18:30 still appears in your 19:00–23:00 prime-time view. Naive datetime objects are assumed to be UTC; use timezone-aware datetimes when your source feed uses a local offset.

⚠️ Detecting Overlapping Broadcasts

Overlapping programmes on the same channel are the classic symptom of malformed or stale guide data. One function surfaces them:

from tvnow_epg import find_overlaps

for earlier, later in find_overlaps(schedule.programmes):
    print(f"{earlier.title} overlaps {later.title} on {earlier.channel_id}")
Enter fullscreen mode Exit fullscreen mode

This is a first-class validation primitive: schedule a nightly find_overlaps() run over your guide and you have an early-warning system for provider data rot.

🧹 Normalizing Messy Metadata

Broadcasters are inconsistent with categories — "kids", "Film", "Children", "Movie". Normalizers fold the common variants together:

from tvnow_epg import normalize_title, normalize_category

normalize_title("  The   News  ")   # "the news"
normalize_category("kids")          # "Children"
normalize_category("Film")          # "Movie"
Enter fullscreen mode Exit fullscreen mode

Normalized titles/categories are what make downstream grouping, deduplication, and "movies tonight" filters reliable.

📤 JSON Export

from tvnow_epg import to_json

text = to_json(schedule)                             # whole schedule
text = to_json(schedule, channel_id="example.news")  # one channel
text = to_json(schedule, indent=None)                # compact output
Enter fullscreen mode Exit fullscreen mode

Timestamps export as ISO-8601 strings with their UTC offset — ready for a web dashboard, a mobile app, or a scheduled cron job.

🧪 Testing and Data Hygiene

The test suite runs entirely offline, and the bundled data/sample_epg.xml contains synthetic example data only — no real broadcaster's schedule. The parser is lenient by design: entries missing an id, a valid start time, or a title are skipped, not fatal, and a document that isn't valid XML (or isn't rooted in <tv>) raises tvnow_epg.ParseError, a ValueError subclass.

⚠️ Limitations to Keep in Mind

  • Supports the common core of XMLTV (channels, programmes, titles, descriptions, categories, episode numbers, ratings); exotic extensions are ignored.
  • Timestamps without an explicit offset are assumed to be UTC.
  • The whole document is held in memory; very large multi-week guides (hundreds of MB) are not the design target.
  • No network functionality of any kind — you supply the EPG data, the library processes it.

🌐 Resources

What does your EPG validation pipeline look like? Drop a comment — especially if you've had to deal with week-long guides in odd timezones.

Top comments (0)