Case Study: How Playlists, Shopping Carts, and Feeds Use Lists

The Big Idea

Every time you add a song to a playlist, drop an item into an online shopping cart, or scroll through a social media feed, you're interacting with a data structure that behaves like a list. The underlying implementations in production systems are far more sophisticated than Python lists — involving databases, distributed caches, and complex algorithms — but the conceptual model is the same ordered, mutable sequence you learned about in this chapter.

This case study explores three real-world systems through the lens of list operations, showing how the same patterns (append, remove, sort, filter, iterate) appear everywhere in software.

System 1: The Music Playlist

The Conceptual Model

A playlist is an ordered collection of tracks that supports:

  • Adding tracks at the end or at a specific position (append, insert)
  • Removing tracks by name or position (remove, pop)
  • Reordering by dragging tracks up or down (remove + insert)
  • Shuffling (randomizing the order)
  • Filtering (play only tracks by a certain artist)

Here's a simplified playlist model:

import random

class Playlist:
    """A simplified playlist using list operations."""

    def __init__(self, name: str):
        self.name = name
        self.tracks: list[tuple[str, str, int]] = []  # (title, artist, duration_sec)

    def add_track(self, title: str, artist: str, duration: int) -> None:
        """Add a track to the end of the playlist."""
        self.tracks.append((title, artist, duration))

    def remove_track(self, title: str) -> bool:
        """Remove a track by title. Returns True if found and removed."""
        for i, (t, a, d) in enumerate(self.tracks):
            if t == title:
                self.tracks.pop(i)
                return True
        return False

    def move_track(self, from_pos: int, to_pos: int) -> None:
        """Move a track from one position to another."""
        if 0 <= from_pos < len(self.tracks) and 0 <= to_pos < len(self.tracks):
            track = self.tracks.pop(from_pos)
            self.tracks.insert(to_pos, track)

    def shuffle(self) -> None:
        """Randomize the track order."""
        random.shuffle(self.tracks)

    def sort_by_artist(self) -> None:
        """Sort tracks alphabetically by artist."""
        self.tracks.sort(key=lambda t: t[1])

    def filter_by_artist(self, artist: str) -> list[tuple[str, str, int]]:
        """Return tracks by a specific artist (doesn't modify playlist)."""
        return [t for t in self.tracks if t[1] == artist]

    def total_duration(self) -> str:
        """Return total playlist duration as 'Xh Ym'."""
        total_sec = sum(d for _, _, d in self.tracks)
        hours, remainder = divmod(total_sec, 3600)
        minutes = remainder // 60
        return f"{hours}h {minutes}m"

    def display(self) -> None:
        """Print the playlist."""
        print(f"\n--- {self.name} ({len(self.tracks)} tracks, {self.total_duration()}) ---")
        for i, (title, artist, dur) in enumerate(self.tracks, start=1):
            minutes, seconds = divmod(dur, 60)
            print(f"  {i:2}. {title} — {artist} [{minutes}:{seconds:02d}]")


# Build a playlist
my_playlist = Playlist("Afternoon Coding")
my_playlist.add_track("Midnight City", "M83", 243)
my_playlist.add_track("Digital Love", "Daft Punk", 301)
my_playlist.add_track("Intro", "The xx", 128)
my_playlist.add_track("Around the World", "Daft Punk", 427)
my_playlist.add_track("Breathe", "Telepopmusik", 290)
my_playlist.display()

Output:

--- Afternoon Coding (5 tracks, 0h 23m) ---
   1. Midnight City — M83 [4:03]
   2. Digital Love — Daft Punk [5:01]
   3. Intro — The xx [2:08]
   4. Around the World — Daft Punk [7:07]
   5. Breathe — Telepopmusik [4:50]

List Operations in Action

# Filter by artist
daft_punk_tracks = my_playlist.filter_by_artist("Daft Punk")
print(f"\nDaft Punk tracks: {len(daft_punk_tracks)}")
for title, artist, dur in daft_punk_tracks:
    print(f"  - {title}")
# Daft Punk tracks: 2
#   - Digital Love
#   - Around the World

# Move a track: bring "Intro" to position 0 (first)
my_playlist.move_track(2, 0)
my_playlist.display()
# Intro is now first

# Sort by artist
my_playlist.sort_by_artist()
my_playlist.display()
# Tracks grouped by artist alphabetically

What This Teaches

  • append() models "add to end of playlist"
  • pop() + insert() models "drag and drop" reordering
  • sort() with key models sorting by any field (artist, title, duration)
  • List comprehensions model filtering without modifying the original
  • Tuple unpacking makes it natural to work with multi-field records

How Real Systems Differ

Real music services like Spotify use database-backed playlist storage, not in-memory lists. But the API — the set of operations a user can perform — maps directly to list operations. When a Spotify developer writes the code to handle "user drags track from position 3 to position 1," they're implementing the same logic as move_track above, just with database transactions instead of in-memory list operations.


System 2: The Shopping Cart

The Conceptual Model

An e-commerce shopping cart is a mutable list of items that supports:

  • Adding items (with quantity)
  • Removing items
  • Updating quantities
  • Calculating totals
  • Applying discounts (filtering and transforming)
def create_cart() -> list[tuple[str, float, int]]:
    """Create an empty shopping cart. Items: (name, price, quantity)."""
    return []


def add_to_cart(cart: list, name: str, price: float, quantity: int = 1) -> None:
    """Add an item to the cart. If already present, increase quantity."""
    for i, (item_name, item_price, item_qty) in enumerate(cart):
        if item_name == name:
            cart[i] = (name, price, item_qty + quantity)
            print(f"Updated {name}: quantity now {item_qty + quantity}")
            return
    cart.append((name, price, quantity))
    print(f"Added {name} (${price:.2f} x {quantity})")


def remove_from_cart(cart: list, name: str) -> None:
    """Remove an item from the cart entirely."""
    for i, (item_name, _, _) in enumerate(cart):
        if item_name == name:
            cart.pop(i)
            print(f"Removed {name}")
            return
    print(f"{name} not in cart")


def cart_total(cart: list) -> float:
    """Calculate the total price of all items in the cart."""
    return sum(price * qty for _, price, qty in cart)


def apply_discount(cart: list, threshold: float, percent: float) -> list:
    """Return a NEW cart with discounted prices for items above threshold."""
    return [
        (name, price * (1 - percent / 100), qty) if price >= threshold
        else (name, price, qty)
        for name, price, qty in cart
    ]


def display_cart(cart: list) -> None:
    """Print the cart contents and total."""
    print("\n--- Shopping Cart ---")
    if not cart:
        print("  (empty)")
        return
    for name, price, qty in cart:
        subtotal = price * qty
        print(f"  {name:<20} ${price:>7.2f} x {qty} = ${subtotal:>8.2f}")
    print(f"  {'':─<20} {'':─>22}")
    print(f"  {'Total':<20} {'':>14} ${cart_total(cart):>8.2f}")


# Shopping session
cart = create_cart()
add_to_cart(cart, "Wireless Mouse", 29.99)
add_to_cart(cart, "USB-C Cable", 12.99, 3)
add_to_cart(cart, "Mechanical Keyboard", 89.99)
add_to_cart(cart, "Monitor Stand", 45.00)
display_cart(cart)

Output:

Added Wireless Mouse ($29.99 x 1)
Added USB-C Cable ($12.99 x 3)
Added Mechanical Keyboard ($89.99 x 1)
Added Monitor Stand ($45.00 x 1)

--- Shopping Cart ---
  Wireless Mouse       $  29.99 x 1 = $   29.99
  USB-C Cable          $  12.99 x 3 = $   38.97
  Mechanical Keyboard  $  89.99 x 1 = $   89.99
  Monitor Stand        $  45.00 x 1 = $   45.00
  ──────────────────── ──────────────────────
  Total                               $  203.95

The Discount Pattern

# Apply 15% discount to items $40 and above
discounted = apply_discount(cart, 40.00, 15)
print("\nAfter discount on items >= $40:")
display_cart(discounted)

# The original cart is unchanged
print("\nOriginal cart (unmodified):")
display_cart(cart)

Notice that apply_discount returns a new list rather than modifying the original. This is intentional — the user might want to compare prices before committing to the discount. This is the "return a new list" vs. "modify in place" design decision from section 8.8.

What This Teaches

  • enumerate() is essential when you need to update items by position
  • List comprehensions with conditional expressions model discount/transformation logic
  • Returning new lists vs. modifying in place is a design decision with real consequences
  • Tuple unpacking (for name, price, qty in cart) makes code readable when items have structure

System 3: The Social Media Feed

The Conceptual Model

A social media feed is a list of posts, ordered by time (or by an algorithm). Key operations:

  • New posts are prepended (newest first) or inserted based on ranking
  • Deleted posts are removed
  • Filtering by content type, author, or date
  • Pagination shows a "page" of posts at a time (slicing)
from datetime import datetime, timedelta

# Simulated feed data: (author, content, timestamp, likes)
feed: list[tuple[str, str, datetime, int]] = [
    ("alice", "Just finished a great book!", datetime(2024, 3, 15, 14, 30), 42),
    ("bob", "Check out my new project", datetime(2024, 3, 15, 13, 0), 18),
    ("charlie", "Beautiful sunset today", datetime(2024, 3, 15, 12, 15), 95),
    ("alice", "Learning Python is fun", datetime(2024, 3, 15, 10, 0), 67),
    ("diana", "Concert was amazing!", datetime(2024, 3, 14, 22, 0), 134),
    ("bob", "Morning coffee thoughts", datetime(2024, 3, 14, 8, 30), 23),
    ("eve", "New recipe: pasta carbonara", datetime(2024, 3, 14, 7, 0), 56),
    ("charlie", "Hiking trip photos", datetime(2024, 3, 13, 16, 45), 88),
]


def new_post(feed: list, author: str, content: str) -> None:
    """Add a new post at the top of the feed."""
    post = (author, content, datetime.now(), 0)
    feed.insert(0, post)


def get_page(feed: list, page_num: int, page_size: int = 3) -> list:
    """Return a 'page' of posts using slicing."""
    start = page_num * page_size
    end = start + page_size
    return feed[start:end]


def filter_by_author(feed: list, author: str) -> list:
    """Return all posts by a specific author."""
    return [post for post in feed if post[0] == author]


def top_posts(feed: list, n: int = 3) -> list:
    """Return the n most-liked posts."""
    return sorted(feed, key=lambda post: post[3], reverse=True)[:n]


def display_feed(posts: list, title: str = "Feed") -> None:
    """Display a list of posts."""
    print(f"\n--- {title} ---")
    for author, content, timestamp, likes in posts:
        time_str = timestamp.strftime("%b %d, %H:%M")
        print(f"  @{author} ({time_str}) [{likes} likes]")
        print(f"    {content}")
        print()


# Display page 1 (first 3 posts)
page1 = get_page(feed, 0)
display_feed(page1, "Page 1")

# Display page 2 (next 3 posts)
page2 = get_page(feed, 1)
display_feed(page2, "Page 2")

Output:

--- Page 1 ---
  @alice (Mar 15, 14:30) [42 likes]
    Just finished a great book!

  @bob (Mar 15, 13:00) [18 likes]
    Check out my new project

  @charlie (Mar 15, 12:15) [95 likes]
    Beautiful sunset today


--- Page 2 ---
  @alice (Mar 15, 10:00) [67 likes]
    Learning Python is fun

  @diana (Mar 14, 22:00) [134 likes]
    Concert was amazing!

  @bob (Mar 14, 08:30) [23 likes]
    Morning coffee thoughts
# Top posts by likes
trending = top_posts(feed, 3)
display_feed(trending, "Trending")

# Alice's posts
alice_posts = filter_by_author(feed, "alice")
display_feed(alice_posts, "Alice's Posts")

Output:

--- Trending ---
  @diana (Mar 14, 22:00) [134 likes]
    Concert was amazing!

  @charlie (Mar 15, 12:15) [95 likes]
    Beautiful sunset today

  @charlie (Mar 13, 16:45) [88 likes]
    Hiking trip photos


--- Alice's Posts ---
  @alice (Mar 15, 14:30) [42 likes]
    Just finished a great book!

  @alice (Mar 15, 10:00) [67 likes]
    Learning Python is fun

What This Teaches

  • insert(0, item) models "prepend" — adding to the front of a feed
  • Slicing models pagination — feed[start:end] returns exactly one page
  • sorted() with key and reverse models ranking algorithms
  • List comprehensions model content filtering
  • datetime objects sort naturally (newer dates are "greater"), so sorting by timestamp works out of the box

Common Patterns Across All Three Systems

Operation Playlist Shopping Cart Social Feed
Add item append() append() insert(0, ...)
Remove item pop(i) pop(i) remove()
Reorder pop() + insert() N/A sort() by algorithm
Filter Comprehension Comprehension Comprehension
Sort sort(key=...) N/A sorted(key=...)
Slice Queue/next track N/A Pagination
Total/aggregate sum() duration sum() price max() likes

The patterns are the same. The data is different. Once you recognize these patterns in one domain, you'll see them everywhere.

Discussion Questions

  1. All three systems store items as tuples within a list. What are the advantages and limitations of this approach? What would improve if we used dictionaries (Chapter 9) or custom classes (Chapter 14) instead?

  2. The shopping cart's apply_discount function returns a new list rather than modifying the original. The playlist's shuffle function modifies the playlist in place. What factors should guide this design decision?

  3. The social feed's get_page function uses slicing to implement pagination. What happens if page_num is larger than the available data? Test it and explain why Python's slicing behavior is helpful here.

  4. Real-world versions of these systems handle millions of items. A Python list with a million elements would make in checks slow (it scans every element). What data structure optimizations do real systems use? (Research: database indexes, hash tables, B-trees.)

  5. All three systems use tuples to represent records. What would break if we used lists instead of tuples for the records? Would anything actually go wrong, or is it just a convention?