Music Playlist Manager with DSA
Build a CLI playlist manager powered by 4 core data structures in Python.
Introduction
30 Second Summary
Every music app you use knows how to queue up the next song, undo your last action, and instantly find any track in your library. Behind the scenes, each of those features relies on a specific data structure.
In this project, you will build a command-line Music Playlist Manager in Python. Each feature you add is powered by a different data structure, giving you hands-on intuition for when and why to choose one over another.
What You'll Build
A Python CLI application that manages songs, playlists, and play queues using four core data structures.
By the end of this project, you'll have:
- Four data structures built from scratch (Stack, Queue, Hash Map, and Linked List), each solving a real playlist feature like undo, play queue, instant search, and flexible ordering.
- A working interactive CLI that ties all four structures together into one app you can run and demo.
- A clear mental model for choosing the right data structure based on which operations need to be fast.
- Secret Mission: Add a shuffle feature that randomizes your play queue using the Fisher-Yates algorithm.
Are there any prerequisites?
You should be comfortable with basic Python concepts like variables, loops, functions, and classes. You do not need any prior experience with data structures or algorithms.
Set Up Your Project
Every feature in your playlist manager will be powered by a different data structure. Before you can build those features, you need a foundation: a project folder, a Python file, and a Song class that represents the music you'll be managing.
In this step, you'll set up your workspace inside Google Antigravity IDE and create the basic building block that every data structure in this project will operate on.
In this step, get ready to:
- Create a playlist-manager project folder.
- Verify Python is working in your terminal.
- Build a Song class to represent individual tracks.
Create your project folder
You need a dedicated folder to hold all your project code. You'll create this on your Desktop so it's easy to find.
- In Google Antigravity IDE, make sure you're in the Editor view. If you see the Agent Manager, click the Open Editor button in the top right corner.
- Open the integrated terminal by pressing Ctrl+` (backtick key, above Tab).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir playlist-manager
cd playlist-manager
- Confirm you're in the right folder by running:
pwd
You should see a path ending in playlist-manager.
- Now open this folder as your workspace. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the playlist-manager folder, and click Open.
The IDE reloads with playlist-manager as your workspace. You should see the folder name in the Explorer sidebar on the left.
Don't see the folder in the sidebar?
Make sure you selected the playlist-manager folder itself, not the Desktop. The folder name should appear at the top of the Explorer panel.
If the IDE opened the Agent Manager instead of the Editor after reopening, click Open Editor in the top right corner.
Help me open a folder in Google Antigravity IDE
Create playlist.py and verify Python
All of your code for this project will live in a single file. This keeps things simple so you can focus on learning data structures without juggling multiple modules.
- Open the integrated terminal again by pressing Ctrl+` (backtick).
- Create your Python file by running this command:
touch playlist.py
- Confirm the file exists by running:
ls
You should see playlist.py in the output.
- Verify your Python installation by running:
python --version
✔️ I see a version number
You're all set. Python is installed and working in your terminal.
ⓧ Command not found
Your system may use python3 instead. Try running:
python3 --version
If this shows a version number, use python3 instead of python for all commands throughout this project.
If neither command works, install Python 3 from python.org before continuing.
- Open playlist.py for editing by clicking on it in the Explorer sidebar on the left.
Build the Song class
Every data structure in this project will store and operate on songs. You need a consistent way to represent a song with its title, artist, and duration.
- In playlist.py, type the following code:
import random
class Song:
"""Represents a single song with title, artist, and duration."""
def __init__(self, title, artist, duration):
self.title = title
self.artist = artist
self.duration = duration
def __str__(self):
return f"{self.title} by {self.artist} ({self.duration})"
What does this code do?
- import random brings in Python's random module. You won't use it yet, but you'll need it later for the shuffle feature.
- The Song class stores three attributes: title, artist, and duration.
- The __str__ method defines how a Song displays when you print it. Instead of seeing something like <Song object at 0x...>, you'll see a friendly format like Bohemian Rhapsody by Queen (5:55).
- Save playlist.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- In the terminal, run your file to verify there are no syntax errors:
python playlist.py
You should see no output and no errors. The script runs cleanly because the class is defined but nothing calls it yet.
Seeing a syntax error?
Check that your f-string in __str__ has matching curly braces and quotes. Make sure the f prefix is directly before the opening quote with no space.
If you used python3 --version earlier, use python3 playlist.py here instead.
Help me fix a syntax error in my Song class
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's what your complete playlist.py should look like at this point:
import random
class Song:
"""Represents a single song with title, artist, and duration."""
def __init__(self, title, artist, duration):
self.title = title
self.artist = artist
self.duration = duration
def __str__(self):
return f"{self.title} by {self.artist} ({self.duration})"
Your foundation is in place. You have a project folder, a working Python file, and a Song class ready to be stored in data structures. Next up, you'll build your first data structure: a Stack that powers undo and redo.
Build the Stack (Undo/Redo)
Your Song class is ready to represent individual tracks. Now you need a way to undo mistakes.
Imagine you accidentally delete your favorite song from the library. You need a way to reverse that action instantly. A Stack is the perfect data structure for this because it follows LIFO (Last In, First Out) order. The most recent action you took is the first one you can undo.
In this step, get ready to:
- Implement a Stack class with push, pop, peek, and is_empty methods.
- Create an ActionHistory class that uses two stacks for undo and redo.
- Test LIFO behavior by pushing and popping values.
Implement the Stack class
A Stack is like a stack of plates. You can only add to the top and remove from the top. This Last-In-First-Out behavior is what makes undo work correctly.
- In playlist.py, scroll to the bottom of the file (below the Song class).
- Add the Stack class by copying the following code below the Song class:
# --- STACK (LIFO) ---
class Stack:
"""A Last-In-First-Out data structure."""
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
return None
return self._items.pop()
def peek(self):
if self.is_empty():
return None
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
What does this code do?
The Stack class uses a Python list (self._items) as its internal storage. The underscore prefix signals that other code should not touch this list directly.
- push() adds an item to the top of the stack using list.append(), which is O(1).
- pop() removes and returns the top item. If the stack is empty, it returns None instead of crashing.
- peek() looks at the top item without removing it.
- is_empty() and size() let you check the stack's state.
Why not just use a plain list?
Python lists already support append() and pop(). Wrapping them in a class enforces the LIFO contract. Other parts of your code can only interact through push() and pop(). Nobody can accidentally insert into the middle or sort the items.
- Save playlist.py.
Seeing an indentation error?
Make sure the # --- STACK (LIFO) --- comment starts at the very left margin, not indented inside the Song class. Each method inside Stack should be indented one level (4 spaces) from the class Stack: line.
Help me fix my Stack class indentation in playlist.py
Create the ActionHistory class
Now you need a class that uses two stacks together. One stack tracks actions you can undo. The other tracks actions you can redo.
- Below your Stack class in playlist.py, add the ActionHistory class:
class ActionHistory:
"""Tracks actions using two stacks for undo/redo functionality."""
def __init__(self):
self.undo_stack = Stack()
self.redo_stack = Stack()
def record_action(self, action_type, song):
self.undo_stack.push((action_type, song))
self.redo_stack = Stack()
def undo(self):
if self.undo_stack.is_empty():
print("Nothing to undo!")
return None
action = self.undo_stack.pop()
self.redo_stack.push(action)
return action
def redo(self):
if self.redo_stack.is_empty():
print("Nothing to redo!")
return None
action = self.redo_stack.pop()
self.undo_stack.push(action)
return action
What does this code do?
ActionHistory coordinates two stacks to manage undo and redo.
- record_action() pushes a tuple of (action_type, song) onto the undo stack. It also resets the redo stack because once you take a new action, your old redo history no longer applies.
- undo() pops the most recent action off the undo stack and pushes it onto the redo stack. This means you can redo it later.
- redo() reverses an undo by moving the action from the redo stack back to the undo stack.
- Save playlist.py.
Seeing a NameError for Stack?
The ActionHistory class must appear below the Stack class in your file. Python reads top to bottom. If ActionHistory tries to use Stack() before Stack is defined, you will get a NameError.
Help me fix the NameError in my ActionHistory class
Test your stack with LIFO verification
Time to prove that your Stack actually follows LIFO order. You will push three items and pop them to confirm they come out in reverse.
- Open the integrated terminal in Google Antigravity IDE by pressing Ctrl+` (macOS and Windows).
- Test your Stack's LIFO behavior by running this command:
python -c "from playlist import Stack; s = Stack(); s.push('first'); s.push('second'); s.push('third'); print(s.pop()); print(s.pop()); print(s.pop())"
- You should see this output:
third
second
first
Why did they come out in reverse?
You pushed first, then second, then third. The third item was the last one pushed, so it is the first one popped. This is LIFO in action. For undo/redo, this means the most recent action is always the first to be undone.
- Now test the ActionHistory class by running:
python -c "from playlist import Stack, Song, ActionHistory; h = ActionHistory(); s = Song('Test', 'Artist', '3:00'); h.record_action('add', s); print('Undo stack size:', h.undo_stack.size()); result = h.undo(); print('Undid:', result[0], result[1])"
- You should see:
Undo stack size: 1
Undid: add Test by Artist (3:00)
The action was recorded on the undo stack, and calling undo() returned it. Your Stack and ActionHistory are working correctly.
Seeing a ModuleNotFoundError?
Make sure your terminal is in the playlist-manager folder where playlist.py lives. Run ls to confirm you can see playlist.py listed. If not, use cd to navigate to the correct folder.
Help me fix my ModuleNotFoundError when importing from playlist.py
✔️ Awesome, I've got everything!
Great. Double check you have saved playlist.py with both the Stack and ActionHistory classes.
ⓧ I'd like to double check the full code
Here is the complete playlist.py file so far. Make sure yours matches:
import random
class Song:
"""Represents a single song with title, artist, and duration."""
def __init__(self, title, artist, duration):
self.title = title
self.artist = artist
self.duration = duration
def __str__(self):
return f"{self.title} by {self.artist} ({self.duration})"
# --- STACK (LIFO) ---
class Stack:
"""A Last-In-First-Out data structure."""
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
return None
return self._items.pop()
def peek(self):
if self.is_empty():
return None
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
class ActionHistory:
"""Tracks actions using two stacks for undo/redo functionality."""
def __init__(self):
self.undo_stack = Stack()
self.redo_stack = Stack()
def record_action(self, action_type, song):
self.undo_stack.push((action_type, song))
self.redo_stack = Stack()
def undo(self):
if self.undo_stack.is_empty():
print("Nothing to undo!")
return None
action = self.undo_stack.pop()
self.redo_stack.push(action)
return action
def redo(self):
if self.redo_stack.is_empty():
print("Nothing to redo!")
return None
action = self.redo_stack.pop()
self.undo_stack.push(action)
return action
You now have a working Stack and an ActionHistory that tracks undo/redo. Next up, you will build a Queue to power the "Up Next" play queue.
Build the Queue (Up Next)
Your Stack handles undo/redo by always grabbing the most recent action first. But a play queue works differently. When you line up three songs, you expect the first song you added to play first.
This is where a Queue comes in. A Queue follows FIFO (First In, First Out) order. In this step, you'll build a Queue class that powers the "Up Next" feature of your playlist manager.
In this step, get ready to:
- Implement a Queue class with core FIFO methods.
- Add display and utility methods for the "Up Next" feature.
- Test that songs come out in the order they went in.
Implement the Queue class
A Stack gives you the most recent item first (LIFO). A Queue gives you the oldest item first (FIFO). Think of a line at a coffee shop. The first person in line gets served first.
What is FIFO?
FIFO stands for First In, First Out. The item that enters the queue first is the item that leaves first.
For a music player, this means the first song you queue is the next song that plays. Exactly what you'd expect from an "Up Next" list.
- In playlist.py, scroll to the bottom of the file (after the ActionHistory class) and add the Queue class by copying this code:
# --- QUEUE (FIFO) ---
class Queue:
"""A First-In-First-Out data structure."""
def __init__(self):
self._items = []
def enqueue(self, item):
# Add item to the back of the queue
self._items.append(item)
def dequeue(self):
# Remove and return item from the front of the queue
if self.is_empty():
return None
return self._items.pop(0)
def peek(self):
# Look at the front item without removing it
if self.is_empty():
return None
return self._items[0]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
What does this code do?
- enqueue() adds an item to the back of the queue using append().
- dequeue() removes and returns the item at the front using pop(0). This is the key FIFO operation.
- peek() lets you see what's next without actually removing it.
- is_empty() and size() are helper methods for checking the queue's state.
Why does dequeue use pop(0)?
pop(0) removes the first element in the list. This makes the queue FIFO: items enter at the back and leave from the front.
For a learning project, this approach is clear and correct. In production code, you'd use collections.deque which provides O(1) popleft() instead of O(n) shifting.
You won't see a change yet. The next chunk adds display methods so you can visualize the queue.
Seeing an indentation error?
Make sure the Queue class starts at the same indentation level as the Stack and ActionHistory classes above it. Each method inside the class should be indented one level deeper.
Help me fix my Queue class indentation.
Add display and utility methods
Your Queue can enqueue and dequeue, but you need a way to see what's in it. These utility methods let you display the queue contents and manage its state.
- Still inside the Queue class, add these methods below size():
def display(self):
# Show all items in the queue with a play indicator
if self.is_empty():
print(" Queue is empty.")
for i, item in enumerate(self._items):
prefix = " \u25b6 " if i == 0 else " "
print(f"{prefix}{i + 1}. {item}")
def to_list(self):
# Convert queue to a regular list (useful for shuffling later)
return list(self._items)
def clear(self):
# Remove all items from the queue
self._items = []
What do these methods do?
- display() prints the queue with a play icon next to the first song (the one that would play next).
- to_list() returns a copy of the queue's contents as a regular Python list. You'll use this later for the shuffle feature.
- clear() empties the queue completely by resetting the internal list.
- Save playlist.py.
Methods appearing outside the class?
These methods must be indented inside the Queue class (one level deeper than the class Queue: line). If they're at the same level as the class definition, Python treats them as standalone functions.
Help me check my Queue class structure.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on to the test.
ⓧ I'd like to double check the full code
Here's the complete playlist.py file so far:
import random
class Song:
"""Represents a single song with title, artist, and duration."""
def __init__(self, title, artist, duration):
self.title = title
self.artist = artist
self.duration = duration
def __str__(self):
return f"{self.title} by {self.artist} ({self.duration})"
# --- STACK (LIFO) ---
class Stack:
"""A Last-In-First-Out data structure."""
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
return None
return self._items.pop()
def peek(self):
if self.is_empty():
return None
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
class ActionHistory:
"""Tracks actions using two stacks for undo/redo functionality."""
def __init__(self):
self.undo_stack = Stack()
self.redo_stack = Stack()
def record_action(self, action_type, song):
self.undo_stack.push((action_type, song))
self.redo_stack = Stack()
def undo(self):
if self.undo_stack.is_empty():
print("Nothing to undo!")
return None
action = self.undo_stack.pop()
self.redo_stack.push(action)
return action
def redo(self):
if self.redo_stack.is_empty():
print("Nothing to redo!")
return None
action = self.redo_stack.pop()
self.undo_stack.push(action)
return action
# --- QUEUE (FIFO) ---
class Queue:
"""A First-In-First-Out data structure."""
def __init__(self):
self._items = []
def enqueue(self, item):
self._items.append(item)
def dequeue(self):
if self.is_empty():
return None
return self._items.pop(0)
def peek(self):
if self.is_empty():
return None
return self._items[0]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
def display(self):
if self.is_empty():
print(" Queue is empty.")
for i, item in enumerate(self._items):
prefix = " \u25b6 " if i == 0 else " "
print(f"{prefix}{i + 1}. {item}")
def to_list(self):
return list(self._items)
def clear(self):
self._items = []
Test FIFO behavior
Your Queue class is complete. Now you need to prove that FIFO actually works. You'll add a temporary test block, run it, and confirm songs come out in the order they went in.
- At the very bottom of playlist.py, add this temporary test code:
# --- TEMPORARY TEST (delete after verifying) ---
song1 = Song("Bohemian Rhapsody", "Queen", "5:55")
song2 = Song("Imagine", "John Lennon", "3:03")
song3 = Song("Billie Jean", "Michael Jackson", "4:54")
queue = Queue()
queue.enqueue(song1)
queue.enqueue(song2)
queue.enqueue(song3)
print("Queue after adding 3 songs:")
queue.display()
print("\nDequeueing songs (should be FIFO order):")
print(f" First out: {queue.dequeue()}")
print(f" Second out: {queue.dequeue()}")
print(f" Third out: {queue.dequeue()}")
print(f" Empty queue dequeue: {queue.dequeue()}")
What does this test prove?
You enqueue Bohemian Rhapsody first, then Imagine, then Billie Jean. If FIFO works, dequeue should return them in that same order. The last dequeue on an empty queue should return None.
- Save playlist.py.
- Run your test in the integrated terminal by running this command:
python playlist.py
- You should see output confirming FIFO order. Bohemian Rhapsody comes out first, then Imagine, then Billie Jean.
What should I see?
Your terminal output should look like this:
Queue after adding 3 songs:
▶ 1. Bohemian Rhapsody by Queen (5:55)
2. Imagine by John Lennon (3:03)
3. Billie Jean by Michael Jackson (4:54)
Dequeueing songs (should be FIFO order):
First out: Bohemian Rhapsody by Queen (5:55)
Second out: Imagine by John Lennon (3:03)
Third out: Billie Jean by Michael Jackson (4:54)
Empty queue dequeue: None
Seeing a NameError?
If you see an error mentioning Song or Queue not being defined, make sure your test code is at the very bottom of the file, below all the class definitions.
If the songs come out in the wrong order, double check that dequeue() uses self._items.pop(0) (removes from the front) and not self._items.pop() (which removes from the back, like a Stack).
Help me debug my Queue test output.
- Once you've confirmed the FIFO order, delete all the test code below the comment # --- TEMPORARY TEST (delete after verifying) ---.
- Save playlist.py again.
Stack vs Queue: the key difference
Your Stack uses pop() which removes from the end (LIFO). Your Queue uses pop(0) which removes from the front (FIFO). That single difference in where you remove from determines the entire behavior of each structure.
Your play queue is ready. You can add songs to the back and play them from the front, just like a real music app. Next up, you'll build instant search using a hash map so finding any song takes a single lookup.
Build the Hash Map (Instant Search)
Your play queue is working perfectly. Now imagine your library has hundreds of songs and you need to find one specific track.
Scanning through every song one by one is slow. Python dictionaries are hash maps under the hood. They let you jump directly to any item by its key in a single operation, O(1) average time. In this step, you'll build a SongLibrary class that uses this power for instant title lookups.
In this step, get ready to:
- Build a SongLibrary class that stores songs in a dictionary keyed by title.
- Add search methods that demonstrate O(1) vs O(n) lookups.
- Test with 5+ songs and compare comparison counts between search approaches.
Build the SongLibrary class with dictionary storage
A Python dictionary maps keys to values using a hash function internally. When you look up a key, Python computes its hash and jumps directly to the correct slot. No scanning required.
What is a hash map?
A hash map converts a key (like a song title) into a numeric index using a hash function. That index tells the structure exactly where to store or find the value.
This is why dictionary lookups are O(1) on average. Instead of checking every item (O(n)), you compute one hash and go straight to the answer.
- In playlist.py, scroll down below your Queue class and add the following SongLibrary class with its core methods:
# --- HASH MAP (Dictionary-based Song Library) ---
class SongLibrary:
"""Stores songs in a hash map (dictionary) for O(1) title lookup."""
def __init__(self):
self._songs_by_title = {}
def add_song(self, song):
self._songs_by_title[song.title.lower()] = song
def search_by_title(self, title):
comparisons = 1
result = self._songs_by_title.get(title.lower())
return result, comparisons
What does this code do?
- _songs_by_title = {} creates an empty dictionary. This is your hash map.
- add_song() stores each song using its lowercase title as the key. Lowercasing ensures searches are case-insensitive.
- search_by_title() uses .get() to jump directly to the song in one operation. That single dictionary lookup is always 1 comparison, hence O(1).
- Save playlist.py.
- Run python playlist.py in the integrated terminal. You should see no output and no errors, confirming the syntax is correct.
Seeing a syntax error?
- Check that the class is indented consistently. Every method inside the class needs one level of indentation.
- Make sure you placed this code below the Queue class, not inside it.
- Verify the triple-quoted docstring has matching """ on both sides.
Help me fix a syntax error in my SongLibrary class.
Add search and utility methods
Your title search is instant because the dictionary knows exactly where to look. But what about searching by artist? There is no artist key in the dictionary, so you have to check every song one by one.
- Add the search_by_artist() method inside your SongLibrary class, directly below search_by_title():
def search_by_artist(self, artist):
comparisons = 0
results = []
for song in self._songs_by_title.values():
comparisons += 1
if song.artist.lower() == artist.lower():
results.append(song)
return results, comparisons
Why is this O(n)?
The dictionary is keyed by title, not by artist. To find songs by a specific artist, the method must loop through every value in the dictionary and check each one. The comparisons counter tracks how many songs it had to inspect.
If you have 100 songs, a title search always takes 1 comparison. An artist search might take up to 100. That is the difference between O(1) and O(n).
- Add the final two utility methods below search_by_artist(), still inside the SongLibrary class:
def get_all_songs(self):
return list(self._songs_by_title.values())
def remove_song(self, title):
key = title.lower()
if key in self._songs_by_title:
song = self._songs_by_title.pop(key)
return song
return None
What do these methods do?
- get_all_songs() returns every song in the library as a list. Useful for displaying the full collection.
- remove_song() deletes a song by its title key. The .pop() method removes the entry and returns it in one step. This is also O(1) since it uses the hash map key.
- Save playlist.py.
You won't see any output yet. The next substep runs a test that makes the O(1) vs O(n) difference visible.
Test the library with 5+ songs
Time to prove the difference between hash map lookup and linear scanning. You will add several songs, then compare how many comparisons each search method needs.
- Add this temporary test code at the very bottom of playlist.py, below the SongLibrary class:
# --- TEMPORARY TEST CODE (delete after testing) ---
if __name__ == "__main__":
library = SongLibrary()
# Add 6 songs to the library
library.add_song(Song("Bohemian Rhapsody", "Queen", "5:55"))
library.add_song(Song("Imagine", "John Lennon", "3:03"))
library.add_song(Song("Hotel California", "Eagles", "6:30"))
library.add_song(Song("Superstition", "Stevie Wonder", "4:26"))
library.add_song(Song("Purple Rain", "Prince", "8:41"))
library.add_song(Song("Respect", "Aretha Franklin", "2:27"))
# Test O(1) title search
print("--- Title Search (Hash Map - O(1)) ---")
song, comparisons = library.search_by_title("Hotel California")
print(f"Found: {song} | Comparisons: {comparisons}")
# Test O(n) artist search
print("\n--- Artist Search (Linear Scan - O(n)) ---")
songs, comparisons = library.search_by_artist("Queen")
print(f"Found: {len(songs)} song(s) | Comparisons: {comparisons}")
for s in songs:
print(f" - {s}")
What does this test demonstrate?
The title search finds "Hotel California" in exactly 1 comparison. It hashes the title and jumps directly to the entry.
The artist search for "Queen" must check all 6 songs to find matches. Even though there is only 1 Queen song, the method cannot know that without scanning everything. That is O(n) in action.
- Save playlist.py and run it with this command:
python playlist.py
You should see the title search reporting 1 comparison, and the artist search reporting 6 comparisons. That is the O(1) vs O(n) difference in real numbers.
Not seeing the expected output?
- If you see a NameError, check that the test code is placed outside the SongLibrary class (no indentation on if __name__).
- If you see 0 comparisons for the artist search, make sure the songs you added use the exact artist name you are searching for.
- If the title search returns None, verify your add_song() method stores with song.title.lower() as the key.
Help me debug my SongLibrary test output.
- Once you have confirmed the output, delete the entire test block (everything from the # --- TEMPORARY TEST CODE comment to the end of the file).
- Save playlist.py again.
✔️ Awesome, I've got everything!
Great. Double check you have saved playlist.py with the test code removed.
ⓧ I'd like to double check the full code
Here is the full playlist.py file as it should look at the end of this step (without the temporary test code):
import random
class Song:
"""Represents a single song with title, artist, and duration."""
def __init__(self, title, artist, duration):
self.title = title
self.artist = artist
self.duration = duration
def __str__(self):
return f"{self.title} by {self.artist} ({self.duration})"
# --- STACK (LIFO) ---
class Stack:
"""A Last-In-First-Out data structure."""
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
return None
return self._items.pop()
def peek(self):
if self.is_empty():
return None
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
class ActionHistory:
"""Tracks actions using two stacks for undo/redo functionality."""
def __init__(self):
self.undo_stack = Stack()
self.redo_stack = Stack()
def record_action(self, action_type, song):
self.undo_stack.push((action_type, song))
self.redo_stack = Stack()
def undo(self):
if self.undo_stack.is_empty():
print("Nothing to undo!")
return None
action = self.undo_stack.pop()
self.redo_stack.push(action)
return action
def redo(self):
if self.redo_stack.is_empty():
print("Nothing to redo!")
return None
action = self.redo_stack.pop()
self.undo_stack.push(action)
return action
# --- QUEUE (FIFO) ---
class Queue:
"""A First-In-First-Out data structure."""
def __init__(self):
self._items = []
def enqueue(self, item):
self._items.append(item)
def dequeue(self):
if self.is_empty():
return None
return self._items.pop(0)
def peek(self):
if self.is_empty():
return None
return self._items[0]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
def display(self):
if self.is_empty():
print(" Queue is empty.")
for i, item in enumerate(self._items):
prefix = " \u25b6 " if i == 0 else " "
print(f"{prefix}{i + 1}. {item}")
def to_list(self):
return list(self._items)
def clear(self):
self._items = []
# --- HASH MAP (Dictionary-based Song Library) ---
class SongLibrary:
"""Stores songs in a hash map (dictionary) for O(1) title lookup."""
def __init__(self):
self._songs_by_title = {}
def add_song(self, song):
self._songs_by_title[song.title.lower()] = song
def search_by_title(self, title):
comparisons = 1
result = self._songs_by_title.get(title.lower())
return result, comparisons
def search_by_artist(self, artist):
comparisons = 0
results = []
for song in self._songs_by_title.values():
comparisons += 1
if song.artist.lower() == artist.lower():
results.append(song)
return results, comparisons
def get_all_songs(self):
return list(self._songs_by_title.values())
def remove_song(self, title):
key = title.lower()
if key in self._songs_by_title:
song = self._songs_by_title.pop(key)
return song
return None
Your song library can now store and search songs instantly by title. Next, you will build a linked list that lets you insert and remove songs anywhere in your playlist without shifting everything around.
Build the Linked List (The Playlist)
Your song library can now store and search songs instantly. But a library alone isn't a playlist. You need a way to arrange songs in a specific order and rearrange them without disrupting the whole sequence.
A linked list solves this problem. Instead of storing songs in a fixed-size array where insertions require shifting everything, each song points to the next one in line. This makes inserting or removing songs anywhere in the playlist efficient.
In this step, get ready to:
- Implement a Node class and a LinkedList class with basic methods.
- Add insert_after() and remove() methods for flexible playlist editing.
- Test your linked list by building, editing, and displaying a playlist.
Learn what a linked list is and implement the Node and LinkedList classes
In a Python list, inserting a song in the middle means every song after it has to shift over by one position. That's an O(n) operation.
A linked list avoids this by storing each song in a separate "node" that holds a pointer to the next node. To insert, you just redirect pointers. No shifting required.
What is a linked list?
A linked list is a chain of nodes where each node stores one piece of data and a reference to the next node. The first node is called the "head."
Once you have a reference to a specific node, inserting or removing the next node takes O(1) time. The trade-off is that finding a node by position requires walking through the chain from the beginning, which is O(n).
The building block of a linked list is a Node. Each node holds one song and a reference to the next node in the chain.
- In playlist.py, scroll to the bottom of your file (below the SongLibrary class) and add the Node class by pasting this code:
# --- LINKED LIST (The Playlist) ---
class Node:
"""A single node in the linked list."""
def __init__(self, song):
self.song = song
self.next = None
What does this code do?
- self.song stores the actual data (a Song object) this node holds.
- self.next is the pointer to the next node in the chain. It starts as None because a new node isn't connected to anything yet.
Now you need the LinkedList class itself to manage these nodes. It will track the head of the chain and let you add songs to the end, display the playlist, and check its size.
- Directly below the Node class, add the LinkedList class with its basic methods by pasting this code:
class LinkedList:
"""A singly linked list representing the playlist order."""
def __init__(self):
self.head = None
self._size = 0
def append(self, song):
new_node = Node(song)
if self.head is None:
self.head = new_node
else:
current = self.head
while current.next is not None:
current = current.next
current.next = new_node
self._size += 1
def display(self):
if self.head is None:
print(" Playlist is empty.")
return
current = self.head
position = 1
while current is not None:
print(f" {position}. {current.song}")
current = current.next
position += 1
def size(self):
return self._size
What does this code do?
- self.head is the entry point to the chain. When the list is empty, it's None.
- append() walks the chain until it finds the last node (where current.next is None), then attaches the new node there.
- display() traverses the entire chain from head to tail, printing each song with its position number.
- self._size tracks the count so you don't have to walk the whole chain just to know how many songs are in the list.
- Save playlist.py.
Seeing an indentation error?
Make sure the LinkedList class starts at the same indentation level as the Node class above it (no extra indentation). All methods inside the class should be indented one level (4 spaces).
Help me fix my LinkedList indentation
Add insert_after() and remove() methods
A playlist that only lets you add songs to the end isn't very useful. You need to insert songs at any position and remove songs from anywhere.
These operations show the real power of a linked list. Inserting after a specific node means redirecting two pointers. No elements need to shift.
- Inside the LinkedList class, add the insert_after() method below the append() method by pasting this code:
def insert_after(self, target_title, song):
current = self.head
while current is not None:
if current.song.title.lower() == target_title.lower():
new_node = Node(song)
new_node.next = current.next
current.next = new_node
self._size += 1
return True
current = current.next
return False
What does this code do?
- insert_after() walks the chain looking for a node whose song title matches target_title.
- Once found, it creates a new node, points the new node's next to what came after the target, then redirects the target's next to the new node. Two pointer changes and the song is inserted.
- It returns True if the insertion succeeded, or False if the target song wasn't found.
Removal is slightly trickier. You need to handle two cases: removing the head node (the first song) and removing a node from the middle or end.
- Below the insert_after() method, add the remove() method by pasting this code:
def remove(self, title):
if self.head is None:
return None
if self.head.song.title.lower() == title.lower():
removed = self.head.song
self.head = self.head.next
self._size -= 1
return removed
current = self.head
while current.next is not None:
if current.next.song.title.lower() == title.lower():
removed = current.next.song
current.next = current.next.next
self._size -= 1
return removed
current = current.next
return None
What does this code do?
- The method first checks if the head node is the one to remove. If so, it advances self.head to the next node.
- For non-head removal, it walks the chain looking one node ahead. When it finds the target, it skips over it by pointing current.next to current.next.next.
- It returns the removed Song object (useful for undo later), or None if the song wasn't found.
- Save playlist.py.
Why look one node ahead for removal?
In a singly linked list, nodes only point forward. To remove a node, you need access to the node BEFORE it so you can redirect its next pointer. That's why the code checks current.next instead of current directly.
Getting an AttributeError on next?
Make sure both insert_after() and remove() are indented inside the LinkedList class (one level of indentation, 4 spaces). If they're at the top level, Python treats them as standalone functions without access to the linked list's nodes.
Help me debug my LinkedList methods
Test your linked list
Time to prove your linked list works. You'll build a 5-song playlist, insert a song in the middle, and remove a song, displaying the results after each operation.
- At the very bottom of playlist.py, add this test code:
# --- Test the Linked List ---
playlist = LinkedList()
# Add 5 songs to the playlist
playlist.append(Song("Bohemian Rhapsody", "Queen", "5:55"))
playlist.append(Song("Hotel California", "Eagles", "6:30"))
playlist.append(Song("Stairway to Heaven", "Led Zeppelin", "8:02"))
playlist.append(Song("Imagine", "John Lennon", "3:03"))
playlist.append(Song("Smells Like Teen Spirit", "Nirvana", "5:01"))
print("--- Initial Playlist ---")
playlist.display()
print(f"Size: {playlist.size()}")
# Insert a song after "Hotel California"
new_song = Song("Comfortably Numb", "Pink Floyd", "6:24")
playlist.insert_after("Hotel California", new_song)
print("\n--- After inserting 'Comfortably Numb' after 'Hotel California' ---")
playlist.display()
print(f"Size: {playlist.size()}")
# Remove a song
removed = playlist.remove("Imagine")
print(f"\n--- After removing '{removed.title}' ---")
playlist.display()
print(f"Size: {playlist.size()}")
What does this test do?
- Creates a playlist with 5 songs appended in order.
- Inserts "Comfortably Numb" after "Hotel California" (position 2), which should appear at position 3.
- Removes "Imagine" from the middle of the playlist.
- Displays the playlist after each change so you can see the structure update in real time.
- Save playlist.py.
- Run the test in the integrated terminal by running:
python playlist.py
- You should see three playlist displays: the initial 5 songs, then 6 songs with "Comfortably Numb" in position 3, then 5 songs again with "Imagine" removed.
What should I see?
Your terminal output should show the initial playlist of 5 songs (Size: 5), then after insertion the playlist grows to 6 with "Comfortably Numb" appearing at position 3. After removing "Imagine", the playlist is back to 5 songs with no gaps in the numbering.
- Once you've confirmed the output looks correct, delete the test code you just added (everything from # --- Test the Linked List --- to the end of the file).
- Save playlist.py.
Seeing a NameError or empty output?
If you see NameError: name 'LinkedList' is not defined, check that your test code is at the bottom of the file, below the LinkedList class definition. Python reads top-to-bottom, so the class must be defined before you use it.
If you see "Playlist is empty" despite adding songs, make sure append() is indented inside the LinkedList class.
Help me debug my linked list test output
✔️ Awesome, I've got everything!
Great. Double check you've saved your file with the test code removed.
ⓧ I'd like to double check the full code
Here's the full playlist.py file so far (without the test code):
import random
class Song:
"""Represents a single song with title, artist, and duration."""
def __init__(self, title, artist, duration):
self.title = title
self.artist = artist
self.duration = duration
def __str__(self):
return f"{self.title} by {self.artist} ({self.duration})"
# --- STACK (LIFO) ---
class Stack:
"""A Last-In-First-Out data structure."""
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
return None
return self._items.pop()
def peek(self):
if self.is_empty():
return None
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
class ActionHistory:
"""Tracks actions using two stacks for undo/redo functionality."""
def __init__(self):
self.undo_stack = Stack()
self.redo_stack = Stack()
def record_action(self, action_type, song):
self.undo_stack.push((action_type, song))
self.redo_stack = Stack()
def undo(self):
if self.undo_stack.is_empty():
print("Nothing to undo!")
return None
action = self.undo_stack.pop()
self.redo_stack.push(action)
return action
def redo(self):
if self.redo_stack.is_empty():
print("Nothing to redo!")
return None
action = self.redo_stack.pop()
self.undo_stack.push(action)
return action
# --- QUEUE (FIFO) ---
class Queue:
"""A First-In-First-Out data structure."""
def __init__(self):
self._items = []
def enqueue(self, item):
self._items.append(item)
def dequeue(self):
if self.is_empty():
return None
return self._items.pop(0)
def peek(self):
if self.is_empty():
return None
return self._items[0]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
def display(self):
if self.is_empty():
print(" Queue is empty.")
for i, item in enumerate(self._items):
prefix = " \u25b6 " if i == 0 else " "
print(f"{prefix}{i + 1}. {item}")
def to_list(self):
return list(self._items)
def clear(self):
self._items = []
# --- HASH MAP (Dictionary-based Song Library) ---
class SongLibrary:
"""Stores songs in a hash map (dictionary) for O(1) title lookup."""
def __init__(self):
self._songs_by_title = {}
def add_song(self, song):
self._songs_by_title[song.title.lower()] = song
def search_by_title(self, title):
comparisons = 1
result = self._songs_by_title.get(title.lower())
return result, comparisons
def search_by_artist(self, artist):
comparisons = 0
results = []
for song in self._songs_by_title.values():
comparisons += 1
if song.artist.lower() == artist.lower():
results.append(song)
return results, comparisons
def get_all_songs(self):
return list(self._songs_by_title.values())
def remove_song(self, title):
key = title.lower()
if key in self._songs_by_title:
song = self._songs_by_title.pop(key)
return song
return None
# --- LINKED LIST (The Playlist) ---
class Node:
"""A single node in the linked list."""
def __init__(self, song):
self.song = song
self.next = None
class LinkedList:
"""A singly linked list representing the playlist order."""
def __init__(self):
self.head = None
self._size = 0
def append(self, song):
new_node = Node(song)
if self.head is None:
self.head = new_node
else:
current = self.head
while current.next is not None:
current = current.next
current.next = new_node
self._size += 1
def insert_after(self, target_title, song):
current = self.head
while current is not None:
if current.song.title.lower() == target_title.lower():
new_node = Node(song)
new_node.next = current.next
current.next = new_node
self._size += 1
return True
current = current.next
return False
def remove(self, title):
if self.head is None:
return None
if self.head.song.title.lower() == title.lower():
removed = self.head.song
self.head = self.head.next
self._size -= 1
return removed
current = self.head
while current.next is not None:
if current.next.song.title.lower() == title.lower():
removed = current.next.song
current.next = current.next.next
self._size -= 1
return removed
current = current.next
return None
def display(self):
if self.head is None:
print(" Playlist is empty.")
return
current = self.head
position = 1
while current is not None:
print(f" {position}. {current.song}")
current = current.next
position += 1
def size(self):
return self._size
Your playlist now uses a real linked list under the hood. Next up, you'll wire all four data structures together into an interactive CLI menu that brings the entire Music Playlist Manager to life.
Wire It All Together with a CLI Menu
You now have four powerful data structures sitting in playlist.py, each built to solve a specific problem.
The missing piece is a way for you (or any user) to actually interact with them. In this step, you will build a PlaylistManager class that combines all four data structures behind a single command-line menu.
In this step, get ready to:
- Build a PlaylistManager class that composes a SongLibrary, LinkedList, Queue, and ActionHistory together.
- Create an interactive menu loop so users can add songs, queue, play, search, undo, and redo.
- Run the full program end-to-end and verify every data structure works in concert.
Build the PlaylistManager class
The PlaylistManager is a composition class. It does not re-implement any data structure. Instead, it holds one instance of each structure you already built and delegates work to them.
- In playlist.py, scroll to the very bottom of the file (after the LinkedList class) and add the following code:
# --- PLAYLIST MANAGER (Combines everything) ---
class PlaylistManager:
"""The main application combining all data structures."""
def __init__(self):
self.library = SongLibrary()
self.playlist = LinkedList()
self.play_queue = Queue()
self.history = ActionHistory()
def add_song_to_library(self):
title = input(" Song title: ").strip()
artist = input(" Artist: ").strip()
duration = input(" Duration (e.g. 3:45): ").strip()
song = Song(title, artist, duration)
self.library.add_song(song)
self.history.record_action("add", song)
print(f" Added '{song}' to library.")
def add_to_playlist(self):
title = input(" Song title to add to playlist: ").strip()
song, _ = self.library.search_by_title(title)
if song is None:
print(f" Song '{title}' not found in library.")
return
self.playlist.append(song)
print(f" Added '{song.title}' to playlist.")
def queue_song(self):
title = input(" Song title to queue: ").strip()
song, _ = self.library.search_by_title(title)
if song is None:
print(f" Song '{title}' not found in library.")
return
self.play_queue.enqueue(song)
print(f" Queued '{song.title}' to play next.")
def play_next(self):
song = self.play_queue.dequeue()
if song is None:
print(" Queue is empty! Nothing to play.")
return
print(f" \u25b6 Now playing: {song}")
What does this code do?
The __init__ method creates one instance of each data structure: a SongLibrary (hash map), a LinkedList (playlist), a Queue (up next), and an ActionHistory (stacks for undo/redo).
Each method delegates to the right structure. For example, add_song_to_library() uses self.library.add_song() (hash map) and self.history.record_action() (stack). The manager does not re-invent anything. It composes.
- You won't see a change yet. The next chunk adds the remaining methods before you can run the program.
- Directly below play_next(), add the search, undo, redo, and display methods:
def search_library(self):
print(" Search by: (1) Title (2) Artist")
choice = input(" Choice: ").strip()
if choice == "1":
title = input(" Enter title: ").strip()
song, comparisons = self.library.search_by_title(title)
if song:
print(f" Found: {song} (comparisons: {comparisons})")
else:
print(f" Not found. (comparisons: {comparisons})")
elif choice == "2":
artist = input(" Enter artist: ").strip()
songs, comparisons = self.library.search_by_artist(artist)
if songs:
print(f" Found {len(songs)} song(s) (comparisons: {comparisons}):")
for s in songs:
print(f" - {s}")
else:
print(f" No songs by '{artist}'. (comparisons: {comparisons})")
def undo(self):
action = self.history.undo()
if action is None:
return
action_type, song = action
if action_type == "add":
self.library.remove_song(song.title)
self.playlist.remove(song.title)
print(f" Undid: add '{song.title}'")
elif action_type == "remove":
self.library.add_song(song)
print(f" Undid: remove '{song.title}' (restored to library)")
def redo(self):
action = self.history.redo()
if action is None:
return
action_type, song = action
if action_type == "add":
self.library.add_song(song)
print(f" Redid: add '{song.title}'")
elif action_type == "remove":
self.library.remove_song(song.title)
print(f" Redid: remove '{song.title}'")
def show_playlist(self):
print("\n --- Your Playlist ---")
self.playlist.display()
def show_queue(self):
print("\n --- Up Next ---")
self.play_queue.display()
What does this code do?
- search_library() lets the user choose between title search (O(1) hash map lookup) and artist search (O(n) scan). It prints how many comparisons each took so you can see the speed difference.
- undo() pops the last action from the undo stack and reverses it. If the last action was "add", it removes the song from both the library and the playlist.
- redo() pops from the redo stack and re-applies the action.
- show_playlist() and show_queue() delegate directly to the linked list and queue display methods you wrote earlier.
- You still won't see a change yet. The next chunk adds the interactive menu that ties all these methods together.
Create the interactive menu loop
The run() method is the entry point for your program. It prints a numbered menu, reads the user's choice, and calls the correct method in a loop until the user exits.
- Directly below show_queue(), add the run() method and the if __name__ block:
def run(self):
print("\n\ud83c\udfb5 Music Playlist Manager \ud83c\udfb5")
print("=" * 35)
while True:
print("\n (1) Add Song to Library")
print(" (2) Add to Playlist")
print(" (3) Queue Song")
print(" (4) Play Next")
print(" (5) Search Library")
print(" (6) Undo")
print(" (7) Redo")
print(" (8) Show Playlist")
print(" (9) Show Queue")
print(" (0) Exit")
choice = input("\n Choose an option: ").strip()
if choice == "1":
self.add_song_to_library()
elif choice == "2":
self.add_to_playlist()
elif choice == "3":
self.queue_song()
elif choice == "4":
self.play_next()
elif choice == "5":
self.search_library()
elif choice == "6":
self.undo()
elif choice == "7":
self.redo()
elif choice == "8":
self.show_playlist()
elif choice == "9":
self.show_queue()
elif choice == "0":
print("\n Thanks for using Music Playlist Manager! \ud83c\udfb6")
break
else:
print(" Invalid option. Try again.")
if __name__ == "__main__":
manager = PlaylistManager()
manager.run()
What does this code do?
- The while True loop keeps the menu running until the user types 0 to exit.
- Each elif branch maps a menu number to the corresponding method you wrote above.
- The if __name__ == "__main__" block ensures the program only runs when you execute the file directly (not when importing it as a module).
- Save playlist.py.
Seeing an indentation error?
Make sure the run() method is indented at the same level as show_queue() (one level inside the PlaylistManager class). The if __name__ block should be at the top level with no indentation.
help me fix an indentation error in my PlaylistManager class
Run and test the full program
Your playlist manager is complete. Time to run it end-to-end and confirm every data structure does its job.
- Run the program in your integrated terminal:
python playlist.py
- You should see the menu header and a numbered list of options.
- Type 1 and press Enter to add a song to the library.
- Enter a title (e.g. Bohemian Rhapsody), an artist (e.g. Queen), and a duration (e.g. 5:55).
- Repeat option 1 to add at least two more songs to the library.
- Type 3 to queue one of your songs.
- Type 4 to play the next song. You should see the song you queued first appear as "Now playing".
- Type 6 to undo your last add. The song should be removed from the library.
- Type 0 to exit the program.
What should I see?
When you queue multiple songs and play them with option 4, they come out in the order you added them (FIFO). When you undo, the most recent action is reversed first (LIFO). These are your Queue and Stack in action.
Program crashes when you choose an option?
Check that each method in PlaylistManager is indented consistently (4 spaces inside the class). A method at the wrong level will cause a NameError or AttributeError when the menu tries to call it.
help me debug my PlaylistManager
✔️ Awesome, I've got everything!
Great. Double check you have saved playlist.py and that the program runs without errors.
ⓧ I'd like to double check the full code
import random
class Song:
"""Represents a single song with title, artist, and duration."""
def __init__(self, title, artist, duration):
self.title = title
self.artist = artist
self.duration = duration
def __str__(self):
return f"{self.title} by {self.artist} ({self.duration})"
# --- STACK (LIFO) ---
class Stack:
"""A Last-In-First-Out data structure."""
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
return None
return self._items.pop()
def peek(self):
if self.is_empty():
return None
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
class ActionHistory:
"""Tracks actions using two stacks for undo/redo functionality."""
def __init__(self):
self.undo_stack = Stack()
self.redo_stack = Stack()
def record_action(self, action_type, song):
self.undo_stack.push((action_type, song))
self.redo_stack = Stack()
def undo(self):
if self.undo_stack.is_empty():
print("Nothing to undo!")
return None
action = self.undo_stack.pop()
self.redo_stack.push(action)
return action
def redo(self):
if self.redo_stack.is_empty():
print("Nothing to redo!")
return None
action = self.redo_stack.pop()
self.undo_stack.push(action)
return action
# --- QUEUE (FIFO) ---
class Queue:
"""A First-In-First-Out data structure."""
def __init__(self):
self._items = []
def enqueue(self, item):
self._items.append(item)
def dequeue(self):
if self.is_empty():
return None
return self._items.pop(0)
def peek(self):
if self.is_empty():
return None
return self._items[0]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
def display(self):
if self.is_empty():
print(" Queue is empty.")
for i, item in enumerate(self._items):
prefix = " \u25b6 " if i == 0 else " "
print(f"{prefix}{i + 1}. {item}")
def to_list(self):
return list(self._items)
def clear(self):
self._items = []
# --- HASH MAP (Dictionary-based Song Library) ---
class SongLibrary:
"""Stores songs in a hash map (dictionary) for O(1) title lookup."""
def __init__(self):
self._songs_by_title = {}
def add_song(self, song):
self._songs_by_title[song.title.lower()] = song
def search_by_title(self, title):
comparisons = 1
result = self._songs_by_title.get(title.lower())
return result, comparisons
def search_by_artist(self, artist):
comparisons = 0
results = []
for song in self._songs_by_title.values():
comparisons += 1
if song.artist.lower() == artist.lower():
results.append(song)
return results, comparisons
def get_all_songs(self):
return list(self._songs_by_title.values())
def remove_song(self, title):
key = title.lower()
if key in self._songs_by_title:
song = self._songs_by_title.pop(key)
return song
return None
# --- LINKED LIST (The Playlist) ---
class Node:
"""A single node in the linked list."""
def __init__(self, song):
self.song = song
self.next = None
class LinkedList:
"""A singly linked list representing the playlist order."""
def __init__(self):
self.head = None
self._size = 0
def append(self, song):
new_node = Node(song)
if self.head is None:
self.head = new_node
else:
current = self.head
while current.next is not None:
current = current.next
current.next = new_node
self._size += 1
def insert_after(self, target_title, song):
current = self.head
while current is not None:
if current.song.title.lower() == target_title.lower():
new_node = Node(song)
new_node.next = current.next
current.next = new_node
self._size += 1
return True
current = current.next
return False
def remove(self, title):
if self.head is None:
return None
if self.head.song.title.lower() == title.lower():
removed = self.head.song
self.head = self.head.next
self._size -= 1
return removed
current = self.head
while current.next is not None:
if current.next.song.title.lower() == title.lower():
removed = current.next.song
current.next = current.next.next
self._size -= 1
return removed
current = current.next
return None
def display(self):
if self.head is None:
print(" Playlist is empty.")
return
current = self.head
position = 1
while current is not None:
print(f" {position}. {current.song}")
current = current.next
position += 1
def size(self):
return self._size
# --- PLAYLIST MANAGER (Combines everything) ---
class PlaylistManager:
"""The main application combining all data structures."""
def __init__(self):
self.library = SongLibrary()
self.playlist = LinkedList()
self.play_queue = Queue()
self.history = ActionHistory()
def add_song_to_library(self):
title = input(" Song title: ").strip()
artist = input(" Artist: ").strip()
duration = input(" Duration (e.g. 3:45): ").strip()
song = Song(title, artist, duration)
self.library.add_song(song)
self.history.record_action("add", song)
print(f" Added '{song}' to library.")
def add_to_playlist(self):
title = input(" Song title to add to playlist: ").strip()
song, _ = self.library.search_by_title(title)
if song is None:
print(f" Song '{title}' not found in library.")
return
self.playlist.append(song)
print(f" Added '{song.title}' to playlist.")
def queue_song(self):
title = input(" Song title to queue: ").strip()
song, _ = self.library.search_by_title(title)
if song is None:
print(f" Song '{title}' not found in library.")
return
self.play_queue.enqueue(song)
print(f" Queued '{song.title}' to play next.")
def play_next(self):
song = self.play_queue.dequeue()
if song is None:
print(" Queue is empty! Nothing to play.")
return
print(f" \u25b6 Now playing: {song}")
def search_library(self):
print(" Search by: (1) Title (2) Artist")
choice = input(" Choice: ").strip()
if choice == "1":
title = input(" Enter title: ").strip()
song, comparisons = self.library.search_by_title(title)
if song:
print(f" Found: {song} (comparisons: {comparisons})")
else:
print(f" Not found. (comparisons: {comparisons})")
elif choice == "2":
artist = input(" Enter artist: ").strip()
songs, comparisons = self.library.search_by_artist(artist)
if songs:
print(f" Found {len(songs)} song(s) (comparisons: {comparisons}):")
for s in songs:
print(f" - {s}")
else:
print(f" No songs by '{artist}'. (comparisons: {comparisons})")
def undo(self):
action = self.history.undo()
if action is None:
return
action_type, song = action
if action_type == "add":
self.library.remove_song(song.title)
self.playlist.remove(song.title)
print(f" Undid: add '{song.title}'")
elif action_type == "remove":
self.library.add_song(song)
print(f" Undid: remove '{song.title}' (restored to library)")
def redo(self):
action = self.history.redo()
if action is None:
return
action_type, song = action
if action_type == "add":
self.library.add_song(song)
print(f" Redid: add '{song.title}'")
elif action_type == "remove":
self.library.remove_song(song.title)
print(f" Redid: remove '{song.title}'")
def show_playlist(self):
print("\n --- Your Playlist ---")
self.playlist.display()
def show_queue(self):
print("\n --- Up Next ---")
self.play_queue.display()
def run(self):
print("\n\ud83c\udfb5 Music Playlist Manager \ud83c\udfb5")
print("=" * 35)
while True:
print("\n (1) Add Song to Library")
print(" (2) Add to Playlist")
print(" (3) Queue Song")
print(" (4) Play Next")
print(" (5) Search Library")
print(" (6) Undo")
print(" (7) Redo")
print(" (8) Show Playlist")
print(" (9) Show Queue")
print(" (0) Exit")
choice = input("\n Choose an option: ").strip()
if choice == "1":
self.add_song_to_library()
elif choice == "2":
self.add_to_playlist()
elif choice == "3":
self.queue_song()
elif choice == "4":
self.play_next()
elif choice == "5":
self.search_library()
elif choice == "6":
self.undo()
elif choice == "7":
self.redo()
elif choice == "8":
self.show_playlist()
elif choice == "9":
self.show_queue()
elif choice == "0":
print("\n Thanks for using Music Playlist Manager! \ud83c\udfb6")
break
else:
print(" Invalid option. Try again.")
if __name__ == "__main__":
manager = PlaylistManager()
manager.run()
Your Music Playlist Manager is fully functional. Every data structure you built from scratch is now solving a real problem. Next up, you will take on a Secret Mission to add shuffle functionality using a classic algorithm.
Secret mission
Add Fisher-Yates Shuffle
Your play queue always plays songs in the order you added them. What if you want to mix things up? Implement a proper shuffle algorithm that guarantees every possible ordering is equally likely.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The playlist-manager folder on your Desktop (contains playlist.py).
Keep everything running
No action needed. Choose this if you're still actively building or want to revisit your data structures code later.
- Your playlist-manager folder stays on your Desktop. You can reopen it in Google Antigravity IDE any time.
Pause - I'll come back to this later
Since this is a local Python project with no running processes, there is nothing to pause. Your files sit on disk until you return.
- Close Google Antigravity IDE to free up memory.
- Your playlist-manager folder remains on your Desktop, ready for next time.
Delete - I don't want to use this again
Remove all project files and start fresh.
- Close Google Antigravity IDE.
- Delete the playlist-manager folder by running the command for your operating system below.
macOS
rm -rf ~/Desktop/playlist-manager
Windows
rmdir /s /q %USERPROFILE%\Desktop\playlist-manager
- Confirm deletion by running ls ~/Desktop (macOS) or dir %USERPROFILE%\Desktop (Windows). You should no longer see playlist-manager in the output.
Nice Work!
Nice Work!
Nice work! You just built a complete Music Playlist Manager powered by four real data structures, each solving a specific problem.
You've learned how to:
- Implement a Stack (LIFO) and a Queue (FIFO) from scratch to power undo/redo and a play queue with O(1) push, pop, enqueue, and dequeue.
- Use a Hash Map (Python dictionary) for O(1) average title lookup and compare it to O(n) artist scanning.
- Build a Linked List from scratch with Node references to support O(1) insertion and removal at any position in the playlist.
- Secret Mission: Added a shuffle feature using the Fisher-Yates algorithm to randomly reorder the play queue with guaranteed uniform probability.
You now have the mental model to look at any programming problem and ask: "What operations do I need to be fast?" and pick the right structure.
Ready to quiz yourself?