Nintendo does not publish one master count of every Switch 2 game. Ask three different trackers how many titles exist on the Nintendo Switch 2 games list and you will get three different numbers, because Nintendo’s own release pages are split by region and by month, and third-party databases each apply their own cutoff for what counts as “released.” That gap is why so many players end up refreshing five tabs a week just to find out whether a game they want shipped yet.

This tutorial fixes that by walking through a small Python project that pulls from Nintendo’s own regional calendars and a couple of maintained game databases, merges the results into a local SQLite file, flags anything new or changed, and can ping a Discord channel the moment a release date moves. By the end you will have a working Nintendo Switch 2 games list tracker that updates itself, plus the troubleshooting notes you need when a site changes its markup and your parser breaks at 2 a.m.

Why the Nintendo Switch 2 Games List Keeps Outrunning Static Guides

October 2026 is a useful example of why a static list goes stale fast. Nintendo’s own October release page lists Kingdom Hearts Collection [I-III] on October 8, Dragon’s Dogma 2: Dark Arisen on October 9, Ratatan and Castlevania: Belmont’s Curse on October 15, three Resident Evil re-releases (Resident Evil 2 Deluxe Edition, Resident Evil 3, and Resident Evil 4 Gold Edition) on October 16, Nintendo Switch Sports Resort and FINAL FANTASY RESONANCE on October 22, Call of Duty: Modern Warfare 4 on October 23, and Minecraft‘s dedicated Switch 2 port on October 27, according to Nintendo’s own news pages. September had its own wave, including Wo Long: Fallen Dynasty Complete Edition on September 3, RuneScape: Dragonwilds on September 15, Fire Emblem: Fortune’s Weave on September 17, and 007 First Light on September 30.

Every one of those dates lives on a different page, and Nintendo refreshes its regional calendars on its own schedule rather than one global feed. A tracker that only checks a single page misses the Direct-driven announcements that land between official calendar updates, like the batch confirmed during the September 2026 Nintendo Direct, which added The Crew Motorfest for October 8, House Flipper 2 and Bluey’s Happy Snap for October 15, and Hot Wheels Infinite Rush for September 10, per a live blog recap of that Direct. Building your own Nintendo Switch 2 games list tracker means you stop relying on any single source catching every update before you do.

October 2026 Release Snapshot: A Working Example

Before building anything, it helps to see the actual shape of the data you’re going to be scraping. Here’s a snapshot of confirmed Switch 2 dates spanning late September into late October 2026, pulled straight from Nintendo’s own regional calendars and cross-checked against the guide pages referenced above. This is exactly the kind of row-per-game structure your tracker’s database will end up holding.

Release dateTitleSource confirming the date
September 3, 2026Wo Long: Fallen Dynasty Complete EditionNintendo UK
September 15, 2026RuneScape: DragonwildsIGN
September 17, 2026Fire Emblem: Fortune’s WeaveIGN
September 30, 2026007 First LightIGN
October 8, 2026Kingdom Hearts Collection [I-III]Nintendo AU / Nintendo UK
October 9, 2026Dragon’s Dogma 2: Dark ArisenNintendo AU / Nintendo UK
October 15, 2026Ratatan; Castlevania: Belmont’s CurseNintendo AU
October 16, 2026Resident Evil 2 Deluxe Edition; Resident Evil 3; Resident Evil 4 Gold EditionNintendo UK
October 22, 2026Nintendo Switch Sports Resort; FINAL FANTASY RESONANCENintendo UK / Nintendo Life
October 23, 2026Call of Duty: Modern Warfare 4Nintendo UK / Nintendo Life
October 27, 2026Minecraft (Switch 2 port)Nintendo UK

Notice that the Fire Emblem: Fortune’s Weave entry is tagged as a Switch 2 exclusive expansion in Nintendo’s own listings, while most of the rest of this window is ports, remasters, or multi-platform releases landing on Switch 2 alongside other consoles. That distinction, exclusive versus multi-platform, is exactly the kind of field your schema in Step 3 needs to capture if you want to filter the Nintendo Switch 2 games list down to just exclusives later.

What You’ll Build in This Tutorial

By the end of this guide you will have a self-contained project folder with a Python script that scrapes two or three public release calendars, normalizes the data into a shared schema, stores it in SQLite so you keep history instead of overwriting it every run, detects which rows are new or have a changed date, and posts a short summary to a Discord webhook or prints it to your terminal. You will also have a command-line filter so you can ask the tracker things like “show me every confirmed Switch 2 exclusive releasing in the next 30 days” without opening a browser.

The approach mirrors what a lot of gaming news trackers already do internally: pull from Nintendo’s own announcements, cross-reference against maintained databases like Nintendo Life’s upcoming games guide and IGN’s Switch 2 release calendar, and let the overlap between sources act as a confidence check. If a title shows up on Nintendo’s own page and on two independent trackers with the same date, you can trust it. If it only shows up on one, flag it as unconfirmed.

This same overlap-checking habit is worth applying any time you’re trying to pin down a specific Switch 2 release claim rather than a whole calendar. When a Modern Warfare 4 Switch 2 port gets benchmarked, or a review of Kingdom Hearts III + Re Mind on Switch 2 goes up, those single data points are exactly the kind of row your tracker should be capturing and timestamping, not just reading once and forgetting.

Prerequisites and Tools You’ll Need

You do not need a powerful machine for this. A laptop with Python installed and a stable connection is enough. Here is exactly what to have ready before Step 1:

  • Python 3.11 or newer (3.12 recommended) installed and on your PATH
  • pip, the package installer that ships with Python
  • requests 2.32.3 for HTTP calls
  • beautifulsoup4 4.12.3 plus lxml for HTML parsing
  • pandas 2.2.3, optional, only needed for the CSV/Sheets export step
  • SQLite 3, which is bundled with Python’s standard library so no separate install is required
  • A text editor or IDE, such as VS Code
  • A free Discord server and webhook URL, optional, only needed for the alert step
  • Basic comfort reading Python and running commands in a terminal

One more prerequisite that matters more than the package versions: check each site’s robots.txt and terms before you point a scraper at it, and keep your request rate low. This tutorial scrapes public, already-published release calendars for personal, non-commercial tracking, polls once every few hours at most, and identifies itself with a descriptive user agent. That is the difference between a reasonable personal tool and something that gets your IP blocked.

Step 1: Set Up Your Project Environment

Start by creating an isolated project folder with a virtual environment, so the packages you install here don’t collide with anything else on your machine.

mkdir switch2-tracker
cd switch2-tracker
python3 -m venv venv
source venv/bin/activate   # on Windows: venv\Scripts\activate

pip install requests==2.32.3 beautifulsoup4==4.12.3 lxml==5.3.0 pandas==2.2.3

Confirm everything installed correctly before moving on:

python3 -c "import requests, bs4, lxml, pandas; print('all good')"

If that prints “all good” with no import errors, your environment is ready. If it throws a ModuleNotFoundError, double check that your virtual environment is actually activated, since a common mistake is installing packages globally and then running the script from inside an unactivated venv where Python can’t see them.

Step 2: Map the Data Sources Behind the Switch 2 Games List

Before writing a line of scraping code, decide which pages you’ll actually read, because that choice determines how resilient your tracker is to layout changes. Nintendo’s own regional news pages are the most authoritative source for confirmed dates, but each region runs on its own page and its own update cadence. Third-party guides fill the gap between those updates and tend to catch Direct-announced titles faster.

SourceWhat it’s best forUpdate patternCaveat
Nintendo regional news pagesConfirmed first-party and third-party datesMonthly roundup pages, refreshed as dates firm upSplit by region (UK, AU, etc.); no single global feed
Nintendo Life upcoming games guideCatching Direct announcements fastUpdated continuously through the monthIncludes “soon,” unconfirmed entries alongside dated ones
IGN Switch 2 release calendarCross-checking North American datesRolling calendar, updated as titles slip or confirmSeparates “known date” from “unknown date” sections
GamesRadar+ upcoming Switch 2 roundupLonger-range view into 2027Periodic feature updatesMixes near-term and speculative far-out entries

For this project you’ll scrape two sources: Nintendo’s own monthly news page as your primary source of truth, and one secondary database page to catch anything Nintendo hasn’t formally listed yet. That two-source overlap is what lets your Nintendo Switch 2 games list tracker flag confidence level instead of just dumping every rumor into one undifferentiated feed.

Step 3: Design the SQLite Schema

A tracker is only useful if it remembers what it saw last time. Storing results in SQLite instead of overwriting a CSV every run means you can diff today’s scrape against last week’s and know exactly what changed.

ColumnTypePurpose
idINTEGER PRIMARY KEYInternal row identifier
titleTEXTGame name as listed by the source
release_dateTEXTISO date string, or NULL if unconfirmed
platformTEXTAlways “Switch 2” for this tracker, kept for future expansion
sourceTEXTWhich page the row came from
first_seenTEXTTimestamp the row first appeared
last_seenTEXTTimestamp of the most recent scrape that still found this row

Here is the setup script that creates the database file and table:

import sqlite3

def init_db(path="switch2_games.db"):
    conn = sqlite3.connect(path)
    cur = conn.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS games (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            release_date TEXT,
            platform TEXT DEFAULT 'Switch 2',
            source TEXT,
            first_seen TEXT,
            last_seen TEXT,
            UNIQUE(title, source)
        )
    """)
    conn.commit()
    return conn

if __name__ == "__main__":
    conn = init_db()
    print("Database ready: switch2_games.db")
    conn.close()

Run this once with python3 db_setup.py. You should see “Database ready: switch2_games.db” printed, and a new file of that name in your project folder. That UNIQUE constraint on title plus source is doing real work here: it stops the same game, from the same page, being inserted twice while still letting the identical title appear once per source so you can compare how two sites describe the same release.

Step 4: Write the Release-Calendar Parser

Nintendo’s monthly news pages are structured as a long article with repeating heading-and-paragraph blocks, one per game. The parser below looks for heading tags, pulls the game title and date out of the heading text, and grabs the first line of body text underneath as a short description.

import requests
from bs4 import BeautifulSoup
import re

HEADERS = {
    "User-Agent": "Switch2TrackerBot/1.0 (personal project; contact: [email protected])"
}

def fetch_page(url):
    resp = requests.get(url, headers=HEADERS, timeout=15)
    resp.raise_for_status()
    return resp.text

def parse_nintendo_calendar(html, source_label):
    soup = BeautifulSoup(html, "lxml")
    entries = []
    date_pattern = re.compile(
        r"(\d{1,2})(?:st|nd|rd|th)?\s*(January|February|March|April|May|June|"
        r"July|August|September|October|November|December)",
        re.IGNORECASE,
    )

    for heading in soup.find_all(["h2", "h3"]):
        text = heading.get_text(strip=True)
        if not text or len(text) < 3:
            continue

        match = date_pattern.search(text)
        title = date_pattern.sub("", text).strip(" -– - ")

        entries.append({
            "title": title,
            "raw_date_text": match.group(0) if match else None,
            "source": source_label,
        })

    return entries

Notice this parser does not try to guess a full ISO date from "9th October" alone, because without a year that string is ambiguous once you're tracking across a rollover like December into January. The next function normalizes that raw text into a real date using the current scrape year as context, then falls forward a year if the parsed date would land in the past relative to today.

from datetime import datetime

MONTHS = {
    "january": 1, "february": 2, "march": 3, "april": 4, "may": 5, "june": 6,
    "july": 7, "august": 8, "september": 9, "october": 10, "november": 11,
    "december": 12,
}

def normalize_date(raw_text, reference_year, today):
    if not raw_text:
        return None

    parts = raw_text.lower().replace(",", "").split()
    day = int("".join(ch for ch in parts[0] if ch.isdigit()))
    month = MONTHS.get(parts[1].strip("."))
    if not month:
        return None

    candidate = datetime(reference_year, month, day)
    if candidate.date() < today.date():
        candidate = datetime(reference_year + 1, month, day)

    return candidate.date().isoformat()

This two-step split, raw text extraction first and normalization second, matters because it keeps your scraper working even when a page's exact phrasing changes slightly. If the normalization logic breaks, you still have the raw date text saved and can fix the parser without re-scraping.

Step 5: Deduplicate and Upsert New Entries

Every time the tracker runs, it should update existing rows rather than duplicate them, and it needs to record when a row was first seen versus last confirmed. This upsert function does both using SQLite's built-in conflict handling.

from datetime import datetime, timezone

def upsert_game(conn, title, release_date, source):
    now = datetime.now(timezone.utc).isoformat()
    cur = conn.cursor()

    cur.execute(
        "SELECT id, release_date FROM games WHERE title = ? AND source = ?",
        (title, source),
    )
    row = cur.fetchone()

    if row is None:
        cur.execute(
            """INSERT INTO games (title, release_date, source, first_seen, last_seen)
               VALUES (?, ?, ?, ?, ?)""",
            (title, release_date, source, now, now),
        )
        conn.commit()
        return "new"

    existing_id, existing_date = row
    if existing_date != release_date:
        cur.execute(
            "UPDATE games SET release_date = ?, last_seen = ? WHERE id = ?",
            (release_date, now, existing_id),
        )
        conn.commit()
        return "date_changed"

    cur.execute("UPDATE games SET last_seen = ? WHERE id = ?", (now, existing_id))
    conn.commit()
    return "unchanged"

The return value, new, date_changed, or unchanged, is what later steps use to decide whether something is worth alerting on. This is the core mechanic that turns a one-off scraper into an actual Nintendo Switch 2 games list tracker: it remembers state between runs instead of treating every scrape as a blank slate.

Step 6: Filter by Exclusive, Region, and Release Window

Once games are sitting in SQLite, querying them is faster than re-scraping, and it's where most of the day-to-day value comes from. Add a small command-line interface so you can ask targeted questions instead of scrolling a spreadsheet.

import argparse
import sqlite3
from datetime import datetime, timedelta

def query_games(conn, days_ahead=None, source=None):
    cur = conn.cursor()
    sql = "SELECT title, release_date, source FROM games WHERE release_date IS NOT NULL"
    params = []

    if days_ahead is not None:
        cutoff = (datetime.now() + timedelta(days=days_ahead)).date().isoformat()
        sql += " AND release_date <= ?"
        params.append(cutoff)

    if source:
        sql += " AND source = ?"
        params.append(source)

    sql += " ORDER BY release_date ASC"
    cur.execute(sql, params)
    return cur.fetchall()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Query the Switch 2 games tracker")
    parser.add_argument("--days", type=int, help="Only show games releasing within N days")
    parser.add_argument("--source", type=str, help="Filter by source label")
    args = parser.parse_args()

    conn = sqlite3.connect("switch2_games.db")
    results = query_games(conn, days_ahead=args.days, source=args.source)

    for title, date, src in results:
        print(f"{date}  {title}  ({src})")

Run python3 query.py --days 14 and you'll get a plain, date-sorted list of everything releasing in the next two weeks, pulled straight from your local database with zero network calls. That speed difference, instant local query versus a fresh scrape every time, is the whole point of persisting data instead of treating the Switch 2 games list as something you re-fetch from scratch on every question.

Step 7: Detect Changes and Send Alerts

The most useful feature of a tracker over a static page is being told when something changes without having to go check yourself. This function posts a short message to a Discord webhook whenever the upsert function above returns "new" or "date_changed."

import requests

def send_discord_alert(webhook_url, title, status, release_date):
    if status == "new":
        message = f"New Switch 2 listing: **{title}** - {release_date or 'date TBA'}"
    elif status == "date_changed":
        message = f"Date update: **{title}** now shows **{release_date}**"
    else:
        return

    payload = {"content": message}
    resp = requests.post(webhook_url, json=payload, timeout=10)
    resp.raise_for_status()

To get a webhook URL, open your Discord server settings, go to Integrations, create a webhook on whichever channel you want alerts in, and copy the URL it generates. Store that URL as an environment variable rather than hardcoding it in the script, since it's effectively a credential that lets anyone post to your channel.

export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/your-id/your-token"

Step 8: Automate the Tracker With Cron

A tracker that only runs when you remember to launch it isn't really tracking anything. On Linux or macOS, cron handles the scheduling with one line. Checking every six hours is frequent enough to catch same-day announcements without hammering any of the source pages.

# Edit your crontab
crontab -e

# Add this line to run the tracker every 6 hours
0 */6 * * * cd /path/to/switch2-tracker && ./venv/bin/python3 main.py >> tracker.log 2>&1

On Windows, Task Scheduler does the same job: create a basic task, set the trigger to repeat every 6 hours, and point the action at your venv's python.exe with main.py as the argument. Either way, redirect output to a log file like the example above, because when the parser eventually breaks (and it will, the moment a source redesigns its page) that log is the first thing you'll want to check.

Caching and Conditional Requests: Scraping Responsibly

Running every six hours on a cron schedule is already gentle, but you can cut unnecessary load even further by using HTTP conditional requests. Most news and guide pages send back an ETag or Last-Modified header. If you store that value and send it back on the next request, the server can respond with a 304 Not Modified and skip sending the full page body when nothing has changed, which saves bandwidth on both ends and makes your tracker noticeably faster on runs where nothing new shipped.

import requests
import json
import os

CACHE_FILE = "etag_cache.json"

def load_etag_cache():
    if os.path.exists(CACHE_FILE):
        with open(CACHE_FILE) as f:
            return json.load(f)
    return {}

def save_etag_cache(cache):
    with open(CACHE_FILE, "w") as f:
        json.dump(cache, f)

def fetch_with_cache(url, headers, cache):
    request_headers = dict(headers)
    if url in cache:
        request_headers["If-None-Match"] = cache[url]

    resp = requests.get(url, headers=request_headers, timeout=15)

    if resp.status_code == 304:
        return None  # nothing changed, skip parsing

    resp.raise_for_status()
    if "ETag" in resp.headers:
        cache[url] = resp.headers["ETag"]

    return resp.text

Wire this into the main run loop and a cycle where nothing changed becomes a near-instant no-op instead of a full parse. It's a small addition, but it's the difference between a tracker that's a good neighbor to the sites it depends on and one that eventually gets itself blocked for being indistinguishable from abusive traffic. Given that this project leans on publicly maintained pages rather than a dedicated API, treating that access as a privilege worth protecting matters more than it might seem.

Step 9: Export a Shareable Switch 2 Games List

Not everyone you share this with wants to run a Python script. A CSV export, or a Google Sheet if you want it live-updating for a friend group or a Discord server, makes the data usable by anyone.

import pandas as pd
import sqlite3

def export_csv(db_path="switch2_games.db", out_path="switch2_games_list.csv"):
    conn = sqlite3.connect(db_path)
    df = pd.read_sql_query(
        "SELECT title, release_date, source, last_seen FROM games ORDER BY release_date ASC",
        conn,
    )
    df.to_csv(out_path, index=False)
    conn.close()
    print(f"Exported {len(df)} rows to {out_path}")

if __name__ == "__main__":
    export_csv()

Running this produces a flat file you can drop straight into Google Sheets via File > Import, or point a lightweight dashboard tool at. If you want it to refresh automatically, add this export call to the end of your main tracker run so the CSV is always current after each cron cycle.

Example Output: What a Clean Run Looks Like

Here's what a successful run of the full tracker looks like in the terminal, scraping both sources, upserting rows, and printing a changelog:

$ python3 main.py
Fetching nintendo.com/en-gb/News/2026/October ...
Fetching nintendolife.com/guides/upcoming-switch-2 ...
Parsed 34 entries from Nintendo (official)
Parsed 41 entries from Nintendo Life (guide)

Changes this run:
  [new]          Castlevania: Belmont's Curse - 2026-10-15
  [date_changed] Beyond the Dark: Nightwatch - 2026-10-21
  [unchanged]    47 other entries

Database now holds 68 unique title/source rows.
Discord alert sent for 2 changes.
Exported 68 rows to switch2_games_list.csv

And here's the corresponding Discord message your channel would receive for that same run:

New Switch 2 listing: Castlevania: Belmont's Curse - 2026-10-15
Date update: Beyond the Dark: Nightwatch now shows 2026-10-21

Common Pitfalls When Building a Game-Release Tracker

A few mistakes show up repeatedly when people build scrapers like this one, and most of them are avoidable once you know to look for them.

  • Treating every heading tag as a game title. Navigation menus, related-article widgets, and newsletter prompts often use h2 or h3 tags too. Filter by checking that the text near a candidate heading actually contains a date pattern or a "Coming to Nintendo Switch 2" phrase before accepting it as a real entry.
  • Hardcoding the current year. A script written in October will silently mislabel every January release as last year's date if you hardcode 2026 instead of deriving the year from context and rolling forward when needed, as shown in Step 4.
  • Scraping too aggressively. Hitting a page every few minutes looks like a denial-of-service attempt from the server's point of view and can get your IP rate-limited or blocked outright. Six-hour intervals are plenty for a release calendar that updates a handful of times a week.
  • Skipping a descriptive User-Agent. Sending requests with Python's default user agent string is one of the fastest ways to get silently blocked by basic bot protection. Identify your script honestly, as in the Step 4 example.
  • Assuming title text matches exactly across sources. Nintendo's own page might list "Dragon's Dogma 2: Dark Arisen" while another source writes "Dragon's Dogma II: Dark Arisen." Normalize punctuation and roman numerals before comparing titles across sources, or you'll get duplicate rows that are really the same game.
  • Forgetting that unconfirmed dates exist. Several trackers list games with a status of "soon" or "TBA" rather than a hard date. Your parser needs an explicit None/NULL path for these, not a crash when the date regex finds nothing.
  • Not handling pagination or "load more" sections. Some guide pages split long lists across multiple sections or hide older entries behind a toggle. Check the full page source, not just what renders above the fold.

Troubleshooting Guide

These are the issues people most often hit once the tracker is running unattended, and how to fix each one.

  • requests.exceptions.ConnectionError on every run. Usually a DNS or network blip, not your code. Wrap the fetch call in a retry loop with a short backoff before giving up and logging the failure.
  • HTTP 403 Forbidden suddenly appearing. The target site likely tightened its bot protection. Check that your User-Agent header is still set, and consider whether your request frequency has crept up without you noticing.
  • Parser returns zero entries after a site redesign. The heading tags or class names you were matching on changed. Open the page in your browser's dev tools, inspect the new structure, and update the selectors in Step 4 accordingly.
  • Dates parsing one year off. Check the rollover logic in normalize_date. If today's date is in December and the page lists a January release, your reference_year needs to increment, not stay the same.
  • sqlite3.IntegrityError on insert. This means the UNIQUE constraint on title and source caught a genuine duplicate. That's working as intended, switch that insert to the upsert_game function from Step 5 instead of a raw INSERT.
  • Discord webhook returns 404. The webhook was deleted or regenerated on Discord's side. Create a fresh one in your server's Integrations settings and update your environment variable.
  • Cron job never runs. Check that you're using the venv's python3 binary with its full path, not a bare "python3" that might resolve to a different, package-less interpreter under cron's minimal environment. Also confirm the working directory is set correctly with the cd in the crontab line.
  • CSV export has fewer rows than you expect. Confirm your SQL query isn't accidentally filtering on release_date IS NOT NULL when you actually want unconfirmed, date-less entries included too.
  • Same game appears twice with slightly different titles. This is the normalization gap mentioned in the pitfalls above. Add a lightweight title-cleaning function that lowercases, strips punctuation, and standardizes roman numerals before any title comparison.

Testing Your Parser Without Hitting the Live Site Every Time

Re-fetching a live page every time you tweak a regex is slow and burns through your request budget for no reason. Save a copy of the HTML once, then test your parser against that local fixture until the logic is solid, and only re-fetch the live page to confirm the final version still works.

# Save a fixture once
curl -s -A "Switch2TrackerBot/1.0" "https://www.nintendo.com/en-gb/News/2026/October/Upcoming-Games-October-2026-3208377.html" -o fixtures/october_2026.html

# Then test against the local file
python3 -c "
from parser import parse_nintendo_calendar
with open('fixtures/october_2026.html') as f:
    html = f.read()
entries = parse_nintendo_calendar(html, 'test')
print(f'Parsed {len(entries)} entries')
for e in entries[:5]:
    print(e)
"

This habit pays off the moment a site changes its layout. Instead of guessing blindly against a live page while burning requests, you diff the new HTML against your saved fixture, see exactly what moved, and adjust your selectors with full visibility into both versions side by side. It's a small workflow change that saves real debugging time once your tracker has been running for a few months and a source inevitably redesigns something.

Advanced Tips for Power Users

Once the basic tracker is stable, a few extensions make it noticeably more useful. Add a confidence score column that counts how many independent sources list the same title and date, so a game confirmed by both Nintendo's own page and a third-party guide ranks above something seen on only one. Add a simple diffing report that runs weekly and summarizes everything that shipped in the last seven days, which doubles as a changelog you can post or share without manually compiling anything.

If you want to expand past Switch 2 specifically, the schema from Step 3 already has a platform column sitting unused, so adding a second parser for a different console's release calendar is mostly a matter of writing a new fetch-and-parse function and reusing everything else. And if you eventually want a web view instead of CLI queries, the SQLite file plugs directly into a minimal Flask or FastAPI app with almost no changes to the data layer you already built.

One more tip worth the extra ten minutes: wrap your main run in a top-level try/except that logs the full traceback to a file before exiting. A scheduled script that silently dies with no log line is the single most common reason people think their tracker is "working fine" for weeks after it actually stopped.

The Complete Working Project

Here's how all the pieces from the steps above come together in a single main.py that you can run end to end. Save the individual functions from each step into their own files (db.py, parser.py, alerts.py, export.py) and import them here, or paste everything into one file if you'd rather keep it simple while you're learning.

import os
import sqlite3
from datetime import datetime

from db import init_db
from parser import fetch_page, parse_nintendo_calendar, normalize_date
from alerts import send_discord_alert
from export import export_csv

SOURCES = {
    "nintendo_official": "https://www.nintendo.com/en-gb/News/2026/October/Upcoming-Games-October-2026-3208377.html",
    "nintendo_life": "https://www.nintendolife.com/guides/upcoming-nintendo-switch-2-games-and-accessories-for-october-and-november-2026",
}

def run():
    conn = init_db()
    today = datetime.now()
    webhook = os.environ.get("DISCORD_WEBHOOK_URL")
    changes = []

    for label, url in SOURCES.items():
        print(f"Fetching {url} ...")
        html = fetch_page(url)
        entries = parse_nintendo_calendar(html, label)
        print(f"Parsed {len(entries)} entries from {label}")

        for entry in entries:
            release_date = normalize_date(entry["raw_date_text"], today.year, today)
            status = upsert_game(conn, entry["title"], release_date, label)
            if status in ("new", "date_changed"):
                changes.append((entry["title"], status, release_date))

    print(f"\nChanges this run: {len(changes)}")
    for title, status, date in changes:
        print(f"  [{status}] {title} - {date}")
        if webhook:
            send_discord_alert(webhook, title, status, date)

    export_csv()
    conn.close()

if __name__ == "__main__":
    run()

That single script, run on a six-hour cron schedule, is a complete Nintendo Switch 2 games list tracker: it fetches, parses, deduplicates, diffs against history, alerts on anything new, and exports a shareable file, all without you touching a browser. It's the same pattern you'd use for tracking any sports schedule, flight price board, or restock calendar, just pointed at game release pages instead.

If you're also keeping tabs on specific titles rather than the whole calendar, this same scraping pattern is worth reusing for watching individual release pages too, whether that's the Monster Hunter Wilds Switch 2 demo window, the Zelda: Ocarina of Time Switch 2 launch rollout, or confirming a port like Call of Duty: Modern Warfare 4 actually hit its stated date on Switch 2 hardware. The parser logic barely changes, you're just pointing it at a narrower page.

The same logic applies to re-releases and remasters, which make up a surprising share of the Switch 2 games list in any given month. A review like the one covering Dragon's Dogma 2: Dark Arisen on Switch 2 often settles performance questions, like whether a port hits its target frame rate, well before Nintendo's own calendar page gets updated with that kind of detail. Feeding those review URLs into your source list alongside the calendar pages gives your tracker a performance-confidence signal, not just a release-confidence one.

For broader coverage of Switch 2 hardware and software news beyond this one tracker, the gaming section is where that kind of reporting lands as it happens, which is useful context if you want to spot-check anything your scraper flags before trusting it fully.

Frequently Asked Questions

Is there an official Nintendo API for the Switch 2 games list?
No. Nintendo publishes release information through regional news articles and its eShop listings, not a public developer API. That's precisely the gap this tutorial's scraper-based approach is built to close.

Why don't you give one total number of Switch 2 games released so far?
Because no single authoritative count exists. Nintendo's regional pages, Nintendo Life, IGN, and GamesRadar+ each use different cutoffs for what counts as confirmed versus rumored, so any single total you'd see quoted is really just one source's definition. Running your own tracker against multiple sources is the more reliable path.

Is scraping Nintendo's website or a fan database legal for personal use?
Scraping publicly accessible pages for personal, non-commercial tracking is common practice, but you should always check the specific site's robots.txt and terms of service, keep request rates low, and avoid republishing scraped content commercially without permission.

How often should the tracker check for updates?
Every six hours is a reasonable default for a release calendar that typically changes a few times per week. There's little practical benefit to checking more often, and doing so increases the odds of triggering rate limiting on the source sites.

Can I adapt this tracker for the original Switch instead of Switch 2?
Yes. The schema and scraping logic are platform-agnostic, you're mostly just pointing the parser at a different set of source URLs and updating the platform column from Step 3.

What if the Discord webhook step feels like overkill?
Skip it entirely. The CLI query tool from Step 6 and the CSV export from Step 9 work completely standalone without any webhook configuration, and are enough for most personal use cases.

My parser works on Nintendo's UK page but fails on the Australian version. Why?
Regional pages sometimes use slightly different HTML templates or date formatting conventions. Test your selectors against each specific regional URL you plan to scrape rather than assuming one region's markup applies everywhere.

How do I know if a listed release date is still accurate?
Check the last_seen timestamp in your database. If a title hasn't been reconfirmed by a recent scrape, treat its date as potentially stale and verify against the source directly before relying on it.