← Back to DevBytes

How to Create a Spotify Playlist Analyzer with Python

What Is a Spotify Playlist Analyzer

A Spotify Playlist Analyzer is a Python application that connects to the Spotify Web API, fetches metadata and audio features for all tracks in a given playlist, and computes meaningful statistics and insights. It can reveal the average tempo, energy, danceability, valence (mood positivity), acousticness, and instrumentalness of a playlist. Beyond raw numbers, a good analyzer can cluster songs by mood, identify outliers, detect dominant genres, and visualize the sonic fingerprint of an entire collection—turning raw listening data into a dashboard that tells you exactly what kind of music lives in that playlist.

Think of it as a sonic microscope: you feed it a playlist URL or ID, and it returns a complete breakdown of musical attributes that Spotify's machine learning models have extracted from the audio signal itself. This gives developers, data enthusiasts, and curious listeners a powerful way to quantify something as subjective as "vibe."

Why Building One Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

There are several practical reasons to build your own playlist analyzer rather than relying on existing tools:

Once you understand the pipeline—authentication, data fetching, feature extraction, and aggregation—you can extend it to compare multiple playlists, track genre distributions, or even build a recommendation system that suggests songs to balance the energy curve of a running mix.

Prerequisites

Before writing any code, ensure you have:

We'll use two core libraries: requests for HTTP calls and python-dotenv to manage secrets safely. Optionally, matplotlib or plotly can be added for visualization later.

Step 1: Getting Spotify API Credentials

The Spotify Web API uses OAuth 2.0. To access it, you need a Client ID and Client Secret obtained from the Spotify Developer Dashboard:

  1. Go to developer.spotify.com/dashboard and log in.
  2. Click "Create App" and give it a name (e.g., "Playlist Analyzer Tutorial").
  3. After creation, click on the app and note the Client ID and Client Secret.
  4. Click "Edit Settings" and add a Redirect URI. For a local script that uses the Authorization Code flow with a temporary local server, use http://localhost:8888/callback. This is crucial—it must match exactly in your code.

Store these credentials in a .env file (never hardcode secrets in source code):

# .env
SPOTIFY_CLIENT_ID=your_client_id_here
SPOTIFY_CLIENT_SECRET=your_client_secret_here
SPOTIFY_REDIRECT_URI=http://localhost:8888/callback

Step 2: Setting Up the Python Environment

Create a project folder and set up a virtual environment:

mkdir playlist-analyzer
cd playlist-analyzer
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

Install the required dependencies:

pip install requests python-dotenv

Create the main script file and a .env file in the project root. Your folder structure should look like:

playlist-analyzer/
├── .env
├── analyzer.py
└── venv/

Step 3: Understanding Spotify OAuth Flow

Spotify's API requires access tokens. For a personal analyzer script, the Authorization Code Flow is ideal because it grants a refresh token that lets your script request new access tokens indefinitely without re-authorizing each time. Here's the flow at a glance:

We'll implement a lightweight local server to catch the callback automatically, avoiding manual copy-paste of the authorization code.

Step 4: Building the Authentication Module

Create a file named auth.py (or include it directly in your main script). This module handles the entire OAuth dance. Here's a complete, reusable implementation:

# auth.py
import os
import sys
import json
import webbrowser
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import requests
from dotenv import load_dotenv

load_dotenv()

CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
REDIRECT_URI = os.getenv("SPOTIFY_REDIRECT_URI", "http://localhost:8888/callback")
SCOPE = "playlist-read-private playlist-read-collaborative"
AUTH_URL = "https://accounts.spotify.com/authorize"
TOKEN_URL = "https://accounts.spotify.com/api/token"
SERVER_PORT = 8888

# Global variable to capture the authorization code
auth_code_received = None

class CallbackHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        global auth_code_received
        parsed = urlparse(self.path)
        if parsed.path == "/callback":
            qs = parse_qs(parsed.query)
            if "code" in qs:
                auth_code_received = qs["code"][0]
                self.send_response(200)
                self.send_header("Content-type", "text/html")
                self.end_headers()
                self.wfile.write(b"<html><body><h1>Authorization successful!</h1><p>You can close this window.</p></body></html>")
                return
        self.send_response(404)
        self.end_headers()

    def log_message(self, format, *args):
        pass  # Suppress console logs for cleaner output

def start_auth_server():
    server = HTTPServer(("localhost", SERVER_PORT), CallbackHandler)
    server.timeout = 120  # Wait up to 2 minutes for the callback
    server.handle_request()
    return auth_code_received

def get_access_token():
    global auth_code_received

    # Check for cached tokens
    if os.path.exists("tokens.json"):
        with open("tokens.json", "r") as f:
            tokens = json.load(f)
        # Try to refresh if access token might be expired
        if "refresh_token" in tokens:
            try:
                response = requests.post(TOKEN_URL, data={
                    "grant_type": "refresh_token",
                    "refresh_token": tokens["refresh_token"],
                    "client_id": CLIENT_ID,
                    "client_secret": CLIENT_SECRET,
                })
                if response.status_code == 200:
                    new_tokens = response.json()
                    new_tokens["refresh_token"] = tokens.get("refresh_token", new_tokens.get("refresh_token"))
                    with open("tokens.json", "w") as f:
                        json.dump(new_tokens, f, indent=2)
                    return new_tokens["access_token"]
                else:
                    print("Refresh failed, re-authorizing...")
            except Exception as e:
                print(f"Token refresh error: {e}")

    # If no valid cached tokens, perform full authorization
    auth_params = {
        "client_id": CLIENT_ID,
        "response_type": "code",
        "redirect_uri": REDIRECT_URI,
        "scope": SCOPE,
    }
    auth_url = f"{AUTH_URL}?{urllib.parse.urlencode(auth_params)}"

    print("Opening browser for Spotify authorization...")
    webbrowser.open(auth_url)
    print("Waiting for callback...")

    code = start_auth_server()
    if not code:
        print("Authorization timed out or was cancelled.")
        sys.exit(1)

    # Exchange code for tokens
    response = requests.post(TOKEN_URL, data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    })

    if response.status_code != 200:
        print(f"Token exchange failed: {response.text}")
        sys.exit(1)

    tokens = response.json()
    with open("tokens.json", "w") as f:
        json.dump(tokens, f, indent=2)

    return tokens["access_token"]

This module does the heavy lifting: it caches tokens in tokens.json, refreshes them silently when possible, and only prompts for browser authorization when absolutely necessary. The local HTTP server captures the redirect callback cleanly without manual intervention.

Step 5: Fetching Playlist Data

Spotify's playlist endpoint returns paginated data. A playlist ID can be extracted from any Spotify playlist URL: the URL https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M has the ID 37i9dQZF1DXcBWIGoYBM5M right after /playlist/.

We need two types of data for each track:

The API endpoints we'll use:

Here's a function that fetches all tracks from a playlist, handling pagination:

def fetch_playlist_tracks(access_token, playlist_id):
    """Fetch all tracks from a playlist, handling pagination."""
    headers = {"Authorization": f"Bearer {access_token}"}
    tracks = []
    url = f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks"
    params = {
        "fields": "items(track(id,name,artists(name),album(name),duration_ms,popularity,explicit)),next",
        "limit": 100
    }

    while url:
        response = requests.get(url, headers=headers, params=params if "?" not in url else None)
        if response.status_code != 200:
            print(f"Error fetching tracks: {response.status_code} - {response.text}")
            break

        data = response.json()
        for item in data.get("items", []):
            track = item.get("track")
            if track and track.get("id"):
                tracks.append({
                    "id": track["id"],
                    "name": track["name"],
                    "artists": [a["name"] for a in track.get("artists", [])],
                    "album": track.get("album", {}).get("name", "Unknown"),
                    "duration_ms": track.get("duration_ms", 0),
                    "popularity": track.get("popularity", 0),
                    "explicit": track.get("explicit", False),
                })
        url = data.get("next")
        params = None  # next URL already contains params

    return tracks

Next, fetch audio features in batches of 100 (Spotify's limit):

def fetch_audio_features_batch(access_token, track_ids):
    """Fetch audio features for a list of track IDs (max 100 per call)."""
    headers = {"Authorization": f"Bearer {access_token}"}
    ids_string = ",".join(track_ids)
    url = f"https://api.spotify.com/v1/audio-features?ids={ids_string}"
    response = requests.get(url, headers=headers)

    if response.status_code != 200:
        print(f"Error fetching audio features: {response.status_code}")
        return []

    features = response.json().get("audio_features", [])
    # Spotify returns null for tracks without audio features
    return [f for f in features if f is not None]

Step 6: Merging and Analyzing the Data

Once you have track metadata and audio features, merge them on track ID and compute statistics. Here's a complete analysis function:

def analyze_playlist(tracks, audio_features_list):
    """Merge track metadata with audio features and compute aggregate stats."""
    # Build a lookup dict from audio features
    features_by_id = {af["id"]: af for af in audio_features_list}

    enriched_tracks = []
    for track in tracks:
        tid = track["id"]
        af = features_by_id.get(tid)
        if af:
            enriched = {**track, **af}
            enriched_tracks.append(enriched)
        else:
            # Keep track even without audio features
            enriched_tracks.append(track)

    if not enriched_tracks:
        return {"error": "No tracks to analyze."}

    # Numeric audio features to aggregate
    numeric_keys = [
        "danceability", "energy", "valence", "acousticness",
        "instrumentalness", "liveness", "speechiness", "tempo", "loudness"
    ]

    stats = {}
    for key in numeric_keys:
        values = [t[key] for t in enriched_tracks if key in t and t[key] is not None]
        if values:
            stats[key] = {
                "mean": sum(values) / len(values),
                "min": min(values),
                "max": max(values),
                "median": sorted(values)[len(values)//2] if values else 0,
            }

    # Key/mode distribution
    key_distribution = {}
    mode_distribution = {"major": 0, "minor": 0}
    for t in enriched_tracks:
        if "key" in t and t["key"] is not None and t["key"] >= 0:
            key_name = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][t["key"] % 12]
            key_distribution[key_name] = key_distribution.get(key_name, 0) + 1
        if "mode" in t and t["mode"] is not None:
            if t["mode"] == 1:
                mode_distribution["major"] += 1
            elif t["mode"] == 0:
                mode_distribution["minor"] += 1

    # Artist frequency
    artist_count = {}
    for t in enriched_tracks:
        for artist in t.get("artists", []):
            artist_count[artist] = artist_count.get(artist, 0) + 1

    top_artists = sorted(artist_count.items(), key=lambda x: x[1], reverse=True)[:10]

    # Top tracks by popularity
    top_tracks = sorted(enriched_tracks, key=lambda t: t.get("popularity", 0), reverse=True)[:10]

    return {
        "total_tracks": len(enriched_tracks),
        "tracks_with_features": len([t for t in enriched_tracks if "energy" in t]),
        "numeric_stats": stats,
        "key_distribution": key_distribution,
        "mode_distribution": mode_distribution,
        "top_artists": top_artists,
        "top_tracks": [(t["name"], t["artists"], t.get("popularity", 0)) for t in top_tracks],
        "enriched_tracks": enriched_tracks,
    }

Step 7: The Complete Analyzer Script

Now we combine authentication, data fetching, and analysis into a single runnable script. Save this as analyzer.py:

# analyzer.py
import os
import json
import sys
import webbrowser
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import requests
from dotenv import load_dotenv

load_dotenv()

CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
REDIRECT_URI = os.getenv("SPOTIFY_REDIRECT_URI", "http://localhost:8888/callback")
SCOPE = "playlist-read-private playlist-read-collaborative"
AUTH_URL = "https://accounts.spotify.com/authorize"
TOKEN_URL = "https://accounts.spotify.com/api/token"
SERVER_PORT = 8888

auth_code_received = None

class CallbackHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        global auth_code_received
        parsed = urlparse(self.path)
        if parsed.path == "/callback":
            qs = parse_qs(parsed.query)
            if "code" in qs:
                auth_code_received = qs["code"][0]
                self.send_response(200)
                self.send_header("Content-type", "text/html")
                self.end_headers()
                self.wfile.write(b"<html><body><h1>Authorization successful!</h1><p>You can close this window.</p></body></html>")
                return
        self.send_response(404)
        self.end_headers()

    def log_message(self, format, *args):
        pass

def start_auth_server():
    server = HTTPServer(("localhost", SERVER_PORT), CallbackHandler)
    server.timeout = 120
    server.handle_request()
    return auth_code_received

def get_access_token():
    global auth_code_received
    if os.path.exists("tokens.json"):
        with open("tokens.json", "r") as f:
            tokens = json.load(f)
        if "refresh_token" in tokens:
            try:
                response = requests.post(TOKEN_URL, data={
                    "grant_type": "refresh_token",
                    "refresh_token": tokens["refresh_token"],
                    "client_id": CLIENT_ID,
                    "client_secret": CLIENT_SECRET,
                })
                if response.status_code == 200:
                    new_tokens = response.json()
                    new_tokens["refresh_token"] = tokens.get("refresh_token", new_tokens.get("refresh_token"))
                    with open("tokens.json", "w") as f:
                        json.dump(new_tokens, f, indent=2)
                    return new_tokens["access_token"]
            except Exception:
                pass

    auth_params = {
        "client_id": CLIENT_ID,
        "response_type": "code",
        "redirect_uri": REDIRECT_URI,
        "scope": SCOPE,
    }
    auth_url = f"{AUTH_URL}?{urllib.parse.urlencode(auth_params)}"
    print("Opening browser for Spotify authorization...")
    webbrowser.open(auth_url)
    print("Waiting for callback...")
    code = start_auth_server()
    if not code:
        print("Authorization timed out.")
        sys.exit(1)

    response = requests.post(TOKEN_URL, data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    })
    if response.status_code != 200:
        print(f"Token exchange failed: {response.text}")
        sys.exit(1)

    tokens = response.json()
    with open("tokens.json", "w") as f:
        json.dump(tokens, f, indent=2)
    return tokens["access_token"]

def fetch_playlist_tracks(access_token, playlist_id):
    headers = {"Authorization": f"Bearer {access_token}"}
    tracks = []
    url = f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks"
    params = {
        "fields": "items(track(id,name,artists(name),album(name),duration_ms,popularity,explicit)),next",
        "limit": 100
    }
    while url:
        response = requests.get(url, headers=headers, params=params if "?" not in url else None)
        if response.status_code != 200:
            print(f"Error fetching tracks: {response.status_code}")
            break
        data = response.json()
        for item in data.get("items", []):
            track = item.get("track")
            if track and track.get("id"):
                tracks.append({
                    "id": track["id"],
                    "name": track["name"],
                    "artists": [a["name"] for a in track.get("artists", [])],
                    "album": track.get("album", {}).get("name", "Unknown"),
                    "duration_ms": track.get("duration_ms", 0),
                    "popularity": track.get("popularity", 0),
                    "explicit": track.get("explicit", False),
                })
        url = data.get("next")
        params = None
    return tracks

def fetch_audio_features_batch(access_token, track_ids):
    headers = {"Authorization": f"Bearer {access_token}"}
    ids_string = ",".join(track_ids)
    url = f"https://api.spotify.com/v1/audio-features?ids={ids_string}"
    response = requests.get(url, headers=headers)
    if response.status_code != 200:
        print(f"Error fetching audio features: {response.status_code}")
        return []
    features = response.json().get("audio_features", [])
    return [f for f in features if f is not None]

def analyze_playlist(tracks, audio_features_list):
    features_by_id = {af["id"]: af for af in audio_features_list}
    enriched_tracks = []
    for track in tracks:
        tid = track["id"]
        af = features_by_id.get(tid)
        if af:
            enriched_tracks.append({**track, **af})
        else:
            enriched_tracks.append(track)

    if not enriched_tracks:
        return {"error": "No tracks to analyze."}

    numeric_keys = [
        "danceability", "energy", "valence", "acousticness",
        "instrumentalness", "liveness", "speechiness", "tempo", "loudness"
    ]
    stats = {}
    for key in numeric_keys:
        values = [t[key] for t in enriched_tracks if key in t and t[key] is not None]
        if values:
            sorted_vals = sorted(values)
            stats[key] = {
                "mean": sum(values) / len(values),
                "min": min(values),
                "max": max(values),
                "median": sorted_vals[len(values)//2],
            }

    key_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
    key_dist = {}
    mode_dist = {"major": 0, "minor": 0}
    for t in enriched_tracks:
        if "key" in t and t["key"] is not None and t["key"] >= 0:
            kn = key_names[t["key"] % 12]
            key_dist[kn] = key_dist.get(kn, 0) + 1
        if "mode" in t and t["mode"] is not None:
            if t["mode"] == 1:
                mode_dist["major"] += 1
            elif t["mode"] == 0:
                mode_dist["minor"] += 1

    artist_count = {}
    for t in enriched_tracks:
        for artist in t.get("artists", []):
            artist_count[artist] = artist_count.get(artist, 0) + 1
    top_artists = sorted(artist_count.items(), key=lambda x: x[1], reverse=True)[:10]

    top_tracks = sorted(enriched_tracks, key=lambda t: t.get("popularity", 0), reverse=True)[:10]

    return {
        "total_tracks": len(enriched_tracks),
        "tracks_with_features": len([t for t in enriched_tracks if "energy" in t]),
        "numeric_stats": stats,
        "key_distribution": key_dist,
        "mode_distribution": mode_dist,
        "top_artists": top_artists,
        "top_tracks": [(t["name"], t["artists"], t.get("popularity", 0)) for t in top_tracks],
        "enriched_tracks": enriched_tracks,
    }

def print_report(results):
    """Print a formatted analysis report to the console."""
    print("\n" + "="*60)
    print("  SPOTIFY PLAYLIST ANALYZER - REPORT")
    print("="*60)
    print(f"\nTotal tracks: {results['total_tracks']}")
    print(f"Tracks with audio features: {results['tracks_with_features']}")

    print("\n--- Numeric Audio Feature Averages ---")
    for feature, data in results["numeric_stats"].items():
        print(f"  {feature.capitalize():>16}: mean={data['mean']:.3f}, median={data['median']:.3f}, "
              f"range=[{data['min']:.3f} - {data['max']:.3f}]")

    print("\n--- Key Distribution ---")
    for key, count in sorted(results["key_distribution"].items(), key=lambda x: x[1], reverse=True):
        print(f"  {key}: {count} tracks")

    print("\n--- Mode Distribution ---")
    total_mode = sum(results["mode_distribution"].values())
    for mode, count in results["mode_distribution"].items():
        pct = (count / total_mode * 100) if total_mode > 0 else 0
        print(f"  {mode}: {count} tracks ({pct:.1f}%)")

    print("\n--- Top 10 Artists ---")
    for i, (artist, count) in enumerate(results["top_artists"], 1):
        print(f"  {i}. {artist} ({count} tracks)")

    print("\n--- Top 10 Most Popular Tracks ---")
    for i, (name, artists, pop) in enumerate(results["top_tracks"], 1):
        print(f"  {i}. {name} - {', '.join(artists)} (Popularity: {pop})")

    print("\n" + "="*60)

def main():
    print("Spotify Playlist Analyzer")
    print("-" * 40)

    # Get playlist ID from user
    playlist_input = input("Enter Spotify playlist URL or ID: ").strip()
    if "playlist/" in playlist_input:
        # Extract ID from URL
        playlist_id = playlist_input.split("playlist/")[1].split("?")[0]
    else:
        playlist_id = playlist_input

    print(f"Analyzing playlist ID: {playlist_id}")

    # Authenticate
    access_token = get_access_token()

    # Fetch tracks
    print("Fetching playlist tracks...")
    tracks = fetch_playlist_tracks(access_token, playlist_id)
    print(f"Found {len(tracks)} tracks.")

    if not tracks:
        print("No tracks found. Check the playlist ID or your access permissions.")
        return

    # Fetch audio features in batches
    print("Fetching audio features...")
    all_features = []
    batch_size = 100
    track_ids = [t["id"] for t in tracks]
    for i in range(0, len(track_ids), batch_size):
        batch = track_ids[i:i+batch_size]
        features = fetch_audio_features_batch(access_token, batch)
        all_features.extend(features)
        print(f"  Fetched features for {i+len(batch)}/{len(track_ids)} tracks")

    print(f"Received audio features for {len(all_features)} tracks.")

    # Analyze
    results = analyze_playlist(tracks, all_features)

    if "error" in results:
        print(f"Analysis error: {results['error']}")
        return

    # Print report
    print_report(results)

    # Optionally save enriched data as JSON
    save_option = input("\nSave enriched track data as JSON? (y/n): ").strip().lower()
    if save_option == "y":
        output_filename = f"playlist_{playlist_id}_analysis.json"
        with open(output_filename, "w", encoding="utf-8") as f:
            json.dump(results, f, indent=2, default=str)
        print(f"Saved to {output_filename}")

if __name__ == "__main__":
    main()

This is the complete, runnable analyzer. When you execute python analyzer.py, it prompts for a playlist URL or ID, handles authentication (opening a browser on first run), fetches all tracks and audio features, computes aggregate statistics, and prints a detailed console report. It also offers to save the full enriched dataset as a JSON file for further exploration.

Step 8: Running the Analyzer

Make sure your .env file is populated with real credentials, then run:

python analyzer.py

Sample interaction:

Spotify Playlist Analyzer
----------------------------------------
Enter Spotify playlist URL or ID: https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M
Analyzing playlist ID: 37i9dQZF1DXcBWIGoYBM5M
Opening browser for Spotify authorization...
Waiting for callback...
Fetching playlist tracks...
Found 50 tracks.
Fetching audio features...
  Fetched features for 50/50 tracks
Received audio features for 50 tracks.

============================================================
  SPOTIFY PLAYLIST ANALYZER - REPORT
============================================================

Total tracks: 50
Tracks with audio features: 50

--- Numeric Audio Feature Averages ---
     Danceability: mean=0.687, median=0.695, range=[0.450 - 0.920]
          Energy: mean=0.723, median=0.745, range=[0.320 - 0.970]
         Valence: mean=0.612, median=0.640, range=[0.180 - 0.950]
    Acousticness: mean=0.141, median=0.085, range=[0.001 - 0.750]
Instrumentalness: mean=0.003, median=0.000, range=[0.000 - 0.050]
       Liveness: mean=0.178, median=0.140, range=[0.030 - 0.610]
     Speechiness: mean=0.061, median=0.045, range=[0.025 - 0.320]
           Tempo: mean=118.450, median=117.500, range=[75.000 - 180.000]
        Loudness: mean=-6.820, median=-6.500, range=[-14.500 - -2.100]

--- Key Distribution ---
  C#: 8 tracks
  G: 7 tracks
  A: 6 tracks
  ...

--- Mode Distribution ---
  major: 32 tracks (64.0%)
  minor: 18 tracks (36.0%)

--- Top 10 Artists ---
  1. Artist Name (4 tracks)
  ...

--- Top 10 Most Popular Tracks ---
  1. Song Title - Artist (Popularity: 95)
  ...

============================================================

Save enriched track data as JSON? (y/n): y
Saved to playlist_37i9dQZF1DXcBWIGoYBM5M_analysis.json

Best Practices for Production-Ready Analyzers

1. Cache API Responses Aggressively

Spotify's API has rate limits (typically a few thousand requests per hour for a personal app). Audio features for a track don't change often. Store fetched audio features in a local SQLite database or a JSON cache keyed by track ID. Before calling the API, check your cache. This dramatically speeds up repeated analyses of overlapping playlists and keeps you well within rate limits.

2. Use Async HTTP for Large Playlists

For playlists with thousands of tracks, synchronous requests can take minutes. Use aiohttp or httpx with async/await to fetch audio feature batches concurrently. A typical pattern fetches 10 batches in parallel, cutting total fetch time by an order of magnitude.

3. Handle Missing Audio Features Gracefully

Not every track on Spotify has audio features available (e.g., very obscure local files or newly added tracks that haven't been processed yet). The API returns null for these entries. Always filter out None values and compute statistics

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles