Basic Music Player
Abstract
A working desktop music player is one of the best mid-beginner projects because it combines four things at once: a graphical user interface (windows, buttons, lists), file-system browsing (loading a folder), audio playback (actually producing sound), and event-driven programming (the program reacts to button clicks instead of running top-to-bottom). The result feels like a real application from the moment it launches.
In this tutorial you will build a music player that:
- Opens a window with playback controls (play, pause, stop, resume).
- Lets the user pick a folder, then lists every audio file inside it.
- Plays the selected song through your speakers.
- Shows which song is currently playing and the player’s status.
By the end you will be comfortable with Tkinter widgets, pygame.mixerpygame.mixer, the osos module for folder listings, and the event loop model that all GUI frameworks share.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- A few
.mp3.mp3(or.wav.wav) files in a folder somewhere on your disk. - Comfort running a Python script from the terminal.
- Familiarity with functions and conditionals (see Simple Calculator if you need a refresher).
What Each Library Does
| Library | Why we use it |
|---|---|
| Tkinter | Standard-library GUI framework. Provides windows, buttons, labels, listboxes. |
tkinter.filedialogtkinter.filedialog | A pre-made directory-picker dialog. |
pygame.mixerpygame.mixer | A sound system that loads and plays audio files. We use Pygame only for the mixer, not for graphics. |
osos | Lists files in a folder, builds file paths in a cross-platform way. |
Tkinter ships with Python. Pygame does not — install it with pip:
pip install pygamepip install pygameConcept: The Event Loop
Up to this point your programs ran top-to-bottom: read input, compute, print, exit. A GUI program is different. It enters a loop managed by Tkinter (root.mainloop()root.mainloop()) that does three things forever:
- Wait for an event (mouse click, keypress, window resize).
- Run the function you registered for that event.
- Repaint the window.
You never write the loop yourself. You just register callbacks — functions that Tkinter will call when something happens. Button(command=play_song)Button(command=play_song) is registering play_songplay_song as the callback for “this button was clicked.”
Getting Started
Create the project
- Create a folder named
basic-music-playerbasic-music-player. - Inside it, create
basicmusicplayer.pybasicmusicplayer.py. - Have a separate folder ready with a handful of music files.
Write the code
Basic Music Player
Source# Basic Music Player
# Credit: https://pythongeeks.org/python-music-player/
# Import Modules
from tkinter import * # pip install tkinter
from tkinter import filedialog
import pygame.mixer as mixer # pip install pygame
import os
# Initializing the mixer
mixer.init()
# Creating the master GUI for python music player
root = Tk()
root.geometry('700x220')
root.title('Basic Music Player')
root.resizable(0, 0)
# Play, Stop, Load and Pause & Resume functions
def play_song(song_name: StringVar, songs_list: Listbox, status: StringVar):
song_name.set(songs_list.get(ACTIVE))
mixer.music.load(songs_list.get(ACTIVE))
mixer.music.play()
status.set("Song PLAYING")
def stop_song(status: StringVar):
mixer.music.stop()
status.set("Song STOPPED")
def load(listbox):
os.chdir(filedialog.askdirectory(title='Open a songs directory'))
tracks = os.listdir()
for track in tracks:
listbox.insert(END, track)
def pause_song(status: StringVar):
mixer.music.pause()
status.set("Song PAUSED")
def resume_song(status: StringVar):
mixer.music.unpause()
status.set("Song RESUMED")
# All the frames
song_frame = LabelFrame(root, text='Current Song', bg='LightBlue', width=400, height=80)
song_frame.place(x=0, y=0)
button_frame = LabelFrame(root, text='Control Buttons', bg='Turquoise', width=400, height=120)
button_frame.place(y=80)
listbox_frame = LabelFrame(root, text='Playlist', bg='RoyalBlue')
listbox_frame.place(x=400, y=0, height=200, width=300)
# All StringVar variables
current_song = StringVar(root, value='<Not selected>')
song_status = StringVar(root, value='<Not Available>')
# Playlist ListBox
playlist = Listbox(listbox_frame, font=('Helvetica', 11), selectbackground='Gold')
scroll_bar = Scrollbar(listbox_frame, orient=VERTICAL)
scroll_bar.pack(side=RIGHT, fill=BOTH)
playlist.config(yscrollcommand=scroll_bar.set)
scroll_bar.config(command=playlist.yview)
playlist.pack(fill=BOTH, padx=5, pady=5)
# SongFrame Labels
Label(song_frame, text='CURRENTLY PLAYING:', bg='LightBlue', font=('Times', 10, 'bold')).place(x=5, y=20)
song_lbl = Label(song_frame, textvariable=current_song, bg='Goldenrod', font=("Times", 12), width=25)
song_lbl.place(x=150, y=20)
# Buttons in the main screen
pause_btn = Button(button_frame, text='Pause', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: pause_song(song_status))
pause_btn.place(x=15, y=10)
stop_btn = Button(button_frame, text='Stop', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: stop_song(song_status))
stop_btn.place(x=105, y=10)
play_btn = Button(button_frame, text='Play', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: play_song(current_song, playlist, song_status))
play_btn.place(x=195, y=10)
resume_btn = Button(button_frame, text='Resume', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: resume_song(song_status))
resume_btn.place(x=285, y=10)
load_btn = Button(button_frame, text='Load Directory', bg='Aqua', font=("Georgia", 13), width=35, command=lambda: load(playlist))
load_btn.place(x=10, y=55)
# Label at the bottom that displays the state of the music
Label(root, textvariable=song_status, bg='SteelBlue', font=('Times', 9), justify=LEFT).pack(side=BOTTOM, fill=X)
# Finalizing the GUI
root.update()
root.mainloop()
# Basic Music Player
# Credit: https://pythongeeks.org/python-music-player/
# Import Modules
from tkinter import * # pip install tkinter
from tkinter import filedialog
import pygame.mixer as mixer # pip install pygame
import os
# Initializing the mixer
mixer.init()
# Creating the master GUI for python music player
root = Tk()
root.geometry('700x220')
root.title('Basic Music Player')
root.resizable(0, 0)
# Play, Stop, Load and Pause & Resume functions
def play_song(song_name: StringVar, songs_list: Listbox, status: StringVar):
song_name.set(songs_list.get(ACTIVE))
mixer.music.load(songs_list.get(ACTIVE))
mixer.music.play()
status.set("Song PLAYING")
def stop_song(status: StringVar):
mixer.music.stop()
status.set("Song STOPPED")
def load(listbox):
os.chdir(filedialog.askdirectory(title='Open a songs directory'))
tracks = os.listdir()
for track in tracks:
listbox.insert(END, track)
def pause_song(status: StringVar):
mixer.music.pause()
status.set("Song PAUSED")
def resume_song(status: StringVar):
mixer.music.unpause()
status.set("Song RESUMED")
# All the frames
song_frame = LabelFrame(root, text='Current Song', bg='LightBlue', width=400, height=80)
song_frame.place(x=0, y=0)
button_frame = LabelFrame(root, text='Control Buttons', bg='Turquoise', width=400, height=120)
button_frame.place(y=80)
listbox_frame = LabelFrame(root, text='Playlist', bg='RoyalBlue')
listbox_frame.place(x=400, y=0, height=200, width=300)
# All StringVar variables
current_song = StringVar(root, value='<Not selected>')
song_status = StringVar(root, value='<Not Available>')
# Playlist ListBox
playlist = Listbox(listbox_frame, font=('Helvetica', 11), selectbackground='Gold')
scroll_bar = Scrollbar(listbox_frame, orient=VERTICAL)
scroll_bar.pack(side=RIGHT, fill=BOTH)
playlist.config(yscrollcommand=scroll_bar.set)
scroll_bar.config(command=playlist.yview)
playlist.pack(fill=BOTH, padx=5, pady=5)
# SongFrame Labels
Label(song_frame, text='CURRENTLY PLAYING:', bg='LightBlue', font=('Times', 10, 'bold')).place(x=5, y=20)
song_lbl = Label(song_frame, textvariable=current_song, bg='Goldenrod', font=("Times", 12), width=25)
song_lbl.place(x=150, y=20)
# Buttons in the main screen
pause_btn = Button(button_frame, text='Pause', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: pause_song(song_status))
pause_btn.place(x=15, y=10)
stop_btn = Button(button_frame, text='Stop', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: stop_song(song_status))
stop_btn.place(x=105, y=10)
play_btn = Button(button_frame, text='Play', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: play_song(current_song, playlist, song_status))
play_btn.place(x=195, y=10)
resume_btn = Button(button_frame, text='Resume', bg='Aqua', font=("Georgia", 13), width=7,
command=lambda: resume_song(song_status))
resume_btn.place(x=285, y=10)
load_btn = Button(button_frame, text='Load Directory', bg='Aqua', font=("Georgia", 13), width=35, command=lambda: load(playlist))
load_btn.place(x=10, y=55)
# Label at the bottom that displays the state of the music
Label(root, textvariable=song_status, bg='SteelBlue', font=('Times', 9), justify=LEFT).pack(side=BOTTOM, fill=X)
# Finalizing the GUI
root.update()
root.mainloop()
Run it
python basicmusicplayer.pypython basicmusicplayer.pyA window appears with Load, Play, Pause, Stop, Resume buttons and an empty playlist. Click Load, pick your music folder, then double-click a song and press Play.
Step-by-Step Explanation
1. Initialize the audio mixer
import pygame.mixer as mixer
mixer.init()import pygame.mixer as mixer
mixer.init()mixer.init()mixer.init() sets up the audio output device. Until you call it, none of the mixer functions work. Calling it more than once is harmless on most systems.
2. Create the main window
from tkinter import *
root = Tk()
root.title("Basic Music Player")
root.geometry("500x350")from tkinter import *
root = Tk()
root.title("Basic Music Player")
root.geometry("500x350")Tk()Tk()creates the top-level window.titletitlesets the title-bar text.geometry("WIDTHxHEIGHT")geometry("WIDTHxHEIGHT")sets the size in pixels.
3. Organize widgets into frames
song_frame = LabelFrame(root, text="Now Playing")
button_frame = LabelFrame(root, text="Controls")
listbox_frame = LabelFrame(root, text="Playlist")song_frame = LabelFrame(root, text="Now Playing")
button_frame = LabelFrame(root, text="Controls")
listbox_frame = LabelFrame(root, text="Playlist")A LabelFrameLabelFrame is a box with a title. Grouping related widgets makes the layout obvious to both the user and the developer.
4. Define playback callbacks
def play_song():
song_name = playlist.get(ACTIVE)
mixer.music.load(song_name)
mixer.music.play()
status_label.config(text="Playing")
def pause_song():
mixer.music.pause()
status_label.config(text="Paused")
def stop_song():
mixer.music.stop()
status_label.config(text="Stopped")
def resume_song():
mixer.music.unpause()
status_label.config(text="Playing")def play_song():
song_name = playlist.get(ACTIVE)
mixer.music.load(song_name)
mixer.music.play()
status_label.config(text="Playing")
def pause_song():
mixer.music.pause()
status_label.config(text="Paused")
def stop_song():
mixer.music.stop()
status_label.config(text="Stopped")
def resume_song():
mixer.music.unpause()
status_label.config(text="Playing")playlist.get(ACTIVE)playlist.get(ACTIVE)returns the currently highlighted entry in the listbox.mixer.music.load(path)mixer.music.load(path)reads the file (no sound yet).mixer.music.play()mixer.music.play()actually starts playback.status_label.config(text=...)status_label.config(text=...)updates the label in real time.
5. Load a directory
from tkinter import filedialog
import os
def load():
directory = filedialog.askdirectory()
os.chdir(directory)
songs = os.listdir(directory)
for song in songs:
if song.lower().endswith((".mp3", ".wav")):
playlist.insert(END, song)from tkinter import filedialog
import os
def load():
directory = filedialog.askdirectory()
os.chdir(directory)
songs = os.listdir(directory)
for song in songs:
if song.lower().endswith((".mp3", ".wav")):
playlist.insert(END, song)filedialog.askdirectory()filedialog.askdirectory()opens the OS file dialog. The function returns the chosen path (or an empty string if the user cancels).os.chdir(directory)os.chdir(directory)makes that folder the working directory somixer.music.load(song)mixer.music.load(song)finds the file with just its name.os.listdir(directory)os.listdir(directory)returns every file/folder name as a list.- The
if song.lower().endswith(...)if song.lower().endswith(...)filter ignores non-audio files likecover.jpgcover.jpgordesktop.inidesktop.ini.
6. Build the playlist widget
playlist = Listbox(listbox_frame, bg="black", fg="white", width=60, height=10)
playlist.pack()playlist = Listbox(listbox_frame, bg="black", fg="white", width=60, height=10)
playlist.pack()A ListboxListbox is the scrollable list on the left of the window. Items are added with playlist.insert(END, "song_name.mp3")playlist.insert(END, "song_name.mp3").
7. Hand control to Tkinter
root.mainloop()root.mainloop()This call never returns until the user closes the window. While it runs, Tkinter listens for clicks and dispatches them to your callbacks.
Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
pygame.error: mixer not initializedpygame.error: mixer not initialized | Forgot mixer.init()mixer.init() | Call it once near the top |
| Window appears but no sound | Wrong working directory or unsupported format | os.chdir(directory)os.chdir(directory) first; stick to MP3/WAV/OGG |
| Listbox is empty after Load | All files filtered out by the extension check | Print os.listdir(directory)os.listdir(directory) to see what is there |
| Window freezes when clicking Play | Long-running code blocking the event loop | Audio playback is non-blocking; if you add other long work, run it on a thread |
_tkinter.TclError: invalid command name_tkinter.TclError: invalid command name | Used a widget after root.destroy()root.destroy() | Do not touch widgets after the window closes |
Variations to Try
1. Volume slider
def set_volume(value):
mixer.music.set_volume(float(value) / 100)
Scale(root, from_=0, to=100, orient=HORIZONTAL, command=set_volume).pack()def set_volume(value):
mixer.music.set_volume(float(value) / 100)
Scale(root, from_=0, to=100, orient=HORIZONTAL, command=set_volume).pack()ScaleScale is Tkinter’s slider; its commandcommand is called with the new value as a string each time the user drags.
2. Display the current track in the title bar
def play_song():
song = playlist.get(ACTIVE)
mixer.music.load(song); mixer.music.play()
root.title(f"Now playing — {song}")def play_song():
song = playlist.get(ACTIVE)
mixer.music.load(song); mixer.music.play()
root.title(f"Now playing — {song}")3. Next / Previous buttons
def next_song():
current = playlist.curselection()
if not current: return
new_index = (current[0] + 1) % playlist.size()
playlist.selection_clear(0, END)
playlist.selection_set(new_index)
playlist.activate(new_index)
play_song()def next_song():
current = playlist.curselection()
if not current: return
new_index = (current[0] + 1) % playlist.size()
playlist.selection_clear(0, END)
playlist.selection_set(new_index)
playlist.activate(new_index)
play_song()4. Shuffle and repeat
- Shuffle: keep a shuffled copy of the indices using
random.samplerandom.sample. - Repeat one: when the song ends, replay it. Detect end with
pygame.mixer.music.get_busy()pygame.mixer.music.get_busy()polled byroot.after(1000, check)root.after(1000, check).
5. Show the duration of each song
Use the mutagenmutagen library (pip install mutagenpip install mutagen) to read MP3 tags:
from mutagen.mp3 import MP3
mp3 = MP3(filename)
duration_seconds = int(mp3.info.length)from mutagen.mp3 import MP3
mp3 = MP3(filename)
duration_seconds = int(mp3.info.length)6. Drag-and-drop loading
Use tkinterDnD2tkinterDnD2 to drop files directly onto the playlist.
7. Save and load playlists
Write the current list of songs to a .txt.txt or .json.json file and load it on start.
Architecture Notes
This script keeps everything at module level for simplicity. For a serious version, wrap state in a class:
class MusicPlayer:
def __init__(self, root):
mixer.init()
self.root = root
self.build_ui()
def build_ui(self):
# create frames, buttons, listbox here
...
def play(self):
...class MusicPlayer:
def __init__(self, root):
mixer.init()
self.root = root
self.build_ui()
def build_ui(self):
# create frames, buttons, listbox here
...
def play(self):
...Why bother? Because a class gives you:
self.current_songself.current_song— clean state without globals.- Testability — instantiate and inspect without launching a window.
- Reusability — embed the player as a widget inside a larger app.
Real-World Applications
- Personal media players for offline music collections.
- Kiosk or installation apps that loop background music.
- Audio-book players with bookmark support.
- White-noise and meditation apps.
- The audio layer of a game (Pygame’s mixer is exactly the same API).
Educational Value
This project teaches:
- GUI fundamentals — widgets, frames, packing, the event loop.
- Event-driven thinking — code runs in response to actions, not top-to-bottom.
- Working with the file system — directories, listings, extensions.
- Multimedia I/O — using a binding (Pygame) to access the OS audio device.
- Separation of UI and logic — even in 100 lines, callbacks isolate concerns.
Next Steps
- Replace the basic Tkinter look with
ttkttkwidgets for native styling. - Add
pygame.mixer.music.set_endeventpygame.mixer.music.set_endeventto auto-advance to the next track. - Display album art by extracting it from MP3 tags with
mutagenmutagen. - Stream audio over the network with
pygame.mixerpygame.mixer+ a remote URL. - Port the same project to PyQt or CustomTkinter for a modern look.
Conclusion
You built a real desktop application with a window, controls, a playlist, and working sound — in roughly a hundred lines of Python. The same patterns (event loop, callbacks, widget composition) power every other Tkinter project on Python Central Hub. The full source is on GitHub. Try one of the variations above and ship your own customized version.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
