What were you listening to this week last year? What about three years ago?

If you're like most people, you have no idea. Those songs are gone — not from Spotify's servers, but from your memory. The playlists you obsessed over in 2022 sit forgotten while your current favourites dominate your queue.

Spotify gives you short-term recaps and recommendations, but it's surprisingly hard to revisit the music that defined earlier chapters of your life. You can remember an era, but not the exact tracks that were on repeat.

In this tutorial, you'll build a tool that fixes that. You'll authenticate with Spotify using OAuth 2.0, fetch your personal listening data, and create playlists programmatically — all in Python.

By the end, you'll have:

  • A working OAuth 2.0 integration with Spotify
  • A script that creates a real playlist in your Spotify account (in about 30 lines of code)
  • An understanding of Spotify's time ranges and track objects
  • The foundation for a "Forgotten Gems" feature that rediscovers songs you loved but stopped listening to

Prerequisites: Basic Python knowledge (variables, functions, lists), and a Spotify account. One important note: since February 2026, registering a Spotify developer app requires the account that owns it to hold an active Spotify Premium subscription, so a free account is no longer enough to follow this tutorial.


Step 1: Get Spotify Developer Credentials

Every application that uses Spotify's API needs credentials: a Client ID and Client Secret. These identify your application to Spotify and enable OAuth authentication.

1. Go to the Spotify Developer Dashboard

Visit https://developer.spotify.com/dashboard and log in with the Spotify account that will own the app.

Spotify's developer rules tightened in 2026. A development-mode app now requires its owner to hold an active Spotify Premium subscription, can grant access to at most five test users, and is limited to one app per developer account. That is plenty for a personal project like this one; reaching a wider audience means applying for extended quota, which Spotify now reserves for registered businesses. Check the dashboard for the current terms.

2. Create an App

Click "Create app" and fill in the form:

  • App name: "Music Time Machine" (or whatever you prefer)
  • App description: "Personal music analytics and playlist generator"
  • Redirect URI: http://127.0.0.1:8888/callback (exactly this — use the loopback IP 127.0.0.1, not localhost, which Spotify no longer accepts)
  • APIs used: Check "Web API"

3. Copy Your Credentials

After creating the app, click "Settings." You'll see your Client ID displayed. Click "View client secret" to reveal your Client Secret. Copy both — you'll need them in a moment.

Why 127.0.0.1:8888/callback? After you authorise the application, Spotify redirects your browser to this URI with an authorisation code. Spotipy (the library we'll use) opens a temporary local server on port 8888 to catch the redirect and extract the code. This is standard OAuth — the redirect URI doesn't need to be a public server. Use the explicit loopback IP 127.0.0.1 rather than localhost: Spotify stopped accepting localhost aliases for redirect URIs, so the two are no longer interchangeable here.


Step 2: Install Dependencies

You need two Python packages: spotipy (a Spotify API wrapper) and python-dotenv (for environment variable management).

pip install "spotipy>=2.26.0" python-dotenv

Spotipy handles the OAuth token exchange, token refresh, and API request formatting behind the scenes. You could build all of this with requests and raw HTTP calls, but Spotipy saves you from writing hundreds of lines of boilerplate for a solved problem. Pin spotipy>=2.26.0: the script below calls current_user_playlist_create(), which older versions don't have.


Step 3: Store Credentials Securely

Create a .env file in your project directory. This file stores secrets and should never be committed to version control.

SPOTIPY_CLIENT_ID=your_client_id_here
SPOTIPY_CLIENT_SECRET=your_client_secret_here
SPOTIPY_REDIRECT_URI=http://127.0.0.1:8888/callback

Replace the placeholder values with your actual credentials from Step 1.

Now add .env to your .gitignore:

.env
__pycache__/
*.pyc
.cache
*.db

The .cache file appears when you run the OAuth flow — it stores your access token locally. Keep that out of version control too.


Step 4: Create Your First Playlist

This is the moment of truth. This script authenticates with Spotify, fetches your top tracks, and creates an actual playlist in your account.

"""
Quick Start: Create your first Spotify playlist
Demonstrates OAuth 2.0 authentication and basic API usage
"""
import os
from dotenv import load_dotenv
import spotipy
from spotipy.oauth2 import SpotifyOAuth

# Load credentials from .env file
load_dotenv()

# Define the permissions we need
scope = "user-top-read playlist-modify-public playlist-modify-private"

# Create Spotify client with OAuth
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))

# Get current user info
user = sp.current_user()
print(f"Authenticated as: {user['display_name']}")

# Fetch top tracks from last 4 weeks
print("\nFetching your top tracks...")
top_tracks = sp.current_user_top_tracks(limit=20, time_range='short_term')

# Extract track URIs (Spotify's unique identifiers)
track_uris = [track['uri'] for track in top_tracks['items']]
track_names = [track['name'] for track in top_tracks['items']]

# Create a new playlist
print("\nCreating playlist...")
playlist = sp.current_user_playlist_create(
    name="My Top Tracks - Quick Start",
    public=False,
    description="Created by Music Time Machine - My current favourites"
)

# Add tracks to the playlist
sp.playlist_add_items(playlist['id'], track_uris)

print(f"\nSuccess! Created playlist with {len(track_uris)} tracks")
print(f"Playlist URL: {playlist['external_urls']['spotify']}")
print("\nYour top tracks right now:")
for i, name in enumerate(track_names, 1):
    print(f"  {i}. {name}")

Save this as quick_start.py and run it:

python quick_start.py

What happens next: Your browser opens to Spotify's authorisation page. You'll see the permissions your app is requesting (read top tracks, create playlists). Click "Agree." Your browser redirects to 127.0.0.1:8888/callback, Spotipy catches the authorisation code, exchanges it for an access token, and the script continues.

Check your Spotify app. The playlist is there. You just integrated with a real-world OAuth API, fetched personalised data, and created content in your account — all in about 30 lines of Python.


What Just Happened: OAuth 2.0 in 60 Seconds

When you ran that script, Spotipy handled a complete OAuth 2.0 Authorization Code flow:

  1. Authorisation request: Spotipy opened your browser to https://accounts.spotify.com/authorize with your Client ID, requested scopes, and redirect URI.
  2. User consent: You saw which permissions the app needs and clicked "Agree."
  3. Authorisation code: Spotify redirected to 127.0.0.1:8888/callback with a temporary authorisation code in the URL.
  4. Token exchange: Spotipy sent the authorisation code (plus your Client ID and Secret) to Spotify's token endpoint and received an access token and refresh token.
  5. API calls: Every subsequent API call (current_user_top_tracks, current_user_playlist_create, etc.) included the access token in the Authorization: Bearer header.
  6. Token cached: Spotipy saved the tokens in a .cache file. Next time you run the script, it reads from cache instead of making you authorise again. When the access token expires (after 1 hour), Spotipy automatically uses the refresh token to get a new one.

That's the same OAuth dance that powers "Sign in with Google," GitHub integrations, and every third-party app that connects to your accounts. The pattern is identical everywhere — only the endpoints change.


Understanding Scopes: What Permissions Mean

Look at the scope variable in the script:

scope = "user-top-read playlist-modify-public playlist-modify-private"

Each scope grants permission for specific operations:

  • user-top-read — Read your top tracks and artists. Without this, sp.current_user_top_tracks() returns a 403 Forbidden error.
  • playlist-modify-public — Create and modify public playlists.
  • playlist-modify-private — Create and modify private playlists. The script creates private playlists (public=False).

This is the principle of least privilege: request only the permissions you need. Spotify users see what you're asking for during the consent screen. If you request user-library-modify (which can delete saved songs) but never use it, that's a red flag.


Going Further: Time Ranges and Forgotten Gems

Here's where it gets interesting. Spotify's current_user_top_tracks() method accepts a time_range parameter:

Time Range Period What It Captures
short_term ~4 weeks Current obsessions
medium_term ~6 months Sustained favourites
long_term Several years All-time patterns

By comparing these time ranges, you can discover songs you've forgotten about:

# Fetch top tracks for each time range
short_term = sp.current_user_top_tracks(limit=50, time_range='short_term')
long_term = sp.current_user_top_tracks(limit=50, time_range='long_term')

# Extract track IDs into sets
short_ids = {track['id'] for track in short_term['items']}
long_ids = {track['id'] for track in long_term['items']}

# Forgotten gems: songs in your long-term favorites
# that you haven't listened to recently
forgotten = long_ids - short_ids

print(f"You loved {len(forgotten)} tracks long-term but haven't heard them recently")

That's Python set subtraction doing something genuinely useful. Your long-term favourites represent songs you listened to repeatedly over years. Your short-term list is what you're playing now. The difference? Songs you provably loved but fell off your rotation.

You could extend this into a full "Forgotten Gems" playlist creator:

# Get the full track details for forgotten gems
forgotten_tracks = [
    track for track in long_term['items']
    if track['id'] in forgotten
]

# Create the playlist
playlist = sp.current_user_playlist_create(
    name="Forgotten Gems",
    public=False,
    description="Songs I loved but forgot about - rediscovered by Music Time Machine"
)

track_uris = [track['uri'] for track in forgotten_tracks]
sp.playlist_add_items(playlist['id'], track_uris)

print(f"\nCreated 'Forgotten Gems' with {len(forgotten_tracks)} tracks")
for track in forgotten_tracks:
    artist = track['artists'][0]['name']
    print(f"  {track['name']} - {artist}")

Going Further: Save Track Objects for Later

The quick-start script creates a playlist from live API data. The full Music Time Machine becomes more useful when you save those track objects over time. Each track response already includes stable identifiers, names, artists, albums, release dates, popularity, external IDs, and Spotify URLs. You can cache the original JSON payload and query it later.

import json
import sqlite3
from datetime import date

conn = sqlite3.connect("music_time_machine.db")
cursor = conn.cursor()

cursor.execute("""
CREATE TABLE IF NOT EXISTS tracks (
    uri TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    artist_name TEXT NOT NULL,
    album_name TEXT,
    popularity INTEGER,
    raw_json TEXT NOT NULL
)
""")

cursor.execute("""
CREATE TABLE IF NOT EXISTS snapshots (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    snapshot_date TEXT NOT NULL,
    time_range TEXT NOT NULL,
    track_uri TEXT NOT NULL,
    rank INTEGER NOT NULL,
    FOREIGN KEY (track_uri) REFERENCES tracks(uri)
)
""")

tracks = sp.current_user_top_tracks(limit=50, time_range="short_term")["items"]

for rank, track in enumerate(tracks, start=1):
    cursor.execute("""
        INSERT OR REPLACE INTO tracks
        (uri, name, artist_name, album_name, popularity, raw_json)
        VALUES (?, ?, ?, ?, ?, ?)
    """, (
        track["uri"],
        track["name"],
        track["artists"][0]["name"],
        track["album"]["name"],
        track.get("popularity"),
        json.dumps(track)
    ))

    cursor.execute("""
        INSERT INTO snapshots (snapshot_date, time_range, track_uri, rank)
        VALUES (?, ?, ?, ?)
    """, (date.today().isoformat(), "short_term", track["uri"], rank))

conn.commit()

That small database unlocks the project's best supported features: monthly snapshots, Forgotten Gems, and analytics over your own listening history. Later, when you migrate to PostgreSQL, the same raw_json column can become JSONB so you can query fields like release dates, popularity, and external IDs directly in SQL.


Where to Go From Here

In this tutorial, you built a working Spotify OAuth integration, created playlists programmatically, and explored time ranges and track objects. That's a solid foundation — but there's a lot more you could build on top of it.

Ideas to explore:

  • Monthly snapshots: Save your top 50 tracks each month into a database. After a few months, you'll have a musical diary you can query — "What was I listening to last March?"
  • Database-powered Forgotten Gems: The set-subtraction approach above works with live API data. With a SQLite database accumulating monthly snapshots, you get much richer history to mine.
  • Evolution analytics: Track how your music taste changes over time. Calculate turnover rates, spot artist shifts, and see which albums keep returning.
  • Playlist sources: Generate playlists from supported history queries, such as forgotten gems, recent discoveries, or your current rotation.

Each of these builds on the same pattern: fetch from Spotify's API, store or compare with historical data, generate something useful.


Want to build the full Music Time Machine?

This tutorial is adapted from Chapter 16 of Mastering APIs With Python, where the complete project is built end-to-end: OAuth authentication, SQLite database design, production features (Forgotten Gems, Monthly Snapshots, Evolution Analytics), error handling with retry logic, and automated testing with mocks. The book takes you from your first API call to deploying production systems on AWS — 30 chapters for €35.

Get the Book