Skip to content

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

LibraryWhy we use it
TkinterStandard-library GUI framework. Provides windows, buttons, labels, listboxes.
tkinter.filedialogtkinter.filedialogA pre-made directory-picker dialog.
pygame.mixerpygame.mixerA sound system that loads and plays audio files. We use Pygame only for the mixer, not for graphics.
ososLists files in a folder, builds file paths in a cross-platform way.

Tkinter ships with Python. Pygame does not — install it with pip:

install
pip install pygame
install
pip install pygame

Concept: 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:

  1. Wait for an event (mouse click, keypress, window resize).
  2. Run the function you registered for that event.
  3. 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

  1. Create a folder named basic-music-playerbasic-music-player.
  2. Inside it, create basicmusicplayer.pybasicmusicplayer.py.
  3. Have a separate folder ready with a handful of music files.

Write the code

Basic Music PlayerSource
Basic Music Player
# 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
# 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

run
python basicmusicplayer.py
run
python basicmusicplayer.py

A 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

basicmusicplayer.py
import pygame.mixer as mixer
mixer.init()
basicmusicplayer.py
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

basicmusicplayer.py
from tkinter import *
root = Tk()
root.title("Basic Music Player")
root.geometry("500x350")
basicmusicplayer.py
from tkinter import *
root = Tk()
root.title("Basic Music Player")
root.geometry("500x350")
  • Tk()Tk() creates the top-level window.
  • titletitle sets the title-bar text.
  • geometry("WIDTHxHEIGHT")geometry("WIDTHxHEIGHT") sets the size in pixels.

3. Organize widgets into frames

basicmusicplayer.py
song_frame = LabelFrame(root, text="Now Playing")
button_frame = LabelFrame(root, text="Controls")
listbox_frame = LabelFrame(root, text="Playlist")
basicmusicplayer.py
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

basicmusicplayer.py
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")
basicmusicplayer.py
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

basicmusicplayer.py
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)
basicmusicplayer.py
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 so mixer.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 like cover.jpgcover.jpg or desktop.inidesktop.ini.

6. Build the playlist widget

basicmusicplayer.py
playlist = Listbox(listbox_frame, bg="black", fg="white", width=60, height=10)
playlist.pack()
basicmusicplayer.py
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

basicmusicplayer.py
root.mainloop()
basicmusicplayer.py
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

ProblemCauseFix
pygame.error: mixer not initializedpygame.error: mixer not initializedForgot mixer.init()mixer.init()Call it once near the top
Window appears but no soundWrong working directory or unsupported formatos.chdir(directory)os.chdir(directory) first; stick to MP3/WAV/OGG
Listbox is empty after LoadAll files filtered out by the extension checkPrint os.listdir(directory)os.listdir(directory) to see what is there
Window freezes when clicking PlayLong-running code blocking the event loopAudio playback is non-blocking; if you add other long work, run it on a thread
_tkinter.TclError: invalid command name_tkinter.TclError: invalid command nameUsed a widget after root.destroy()root.destroy()Do not touch widgets after the window closes

Variations to Try

1. Volume slider

volume.py
def set_volume(value):
    mixer.music.set_volume(float(value) / 100)
 
Scale(root, from_=0, to=100, orient=HORIZONTAL, command=set_volume).pack()
volume.py
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

title.py
def play_song():
    song = playlist.get(ACTIVE)
    mixer.music.load(song); mixer.music.play()
    root.title(f"Now playing — {song}")
title.py
def play_song():
    song = playlist.get(ACTIVE)
    mixer.music.load(song); mixer.music.play()
    root.title(f"Now playing — {song}")

3. Next / Previous buttons

nav.py
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()
nav.py
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 by root.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:

duration.py
from mutagen.mp3 import MP3
mp3 = MP3(filename)
duration_seconds = int(mp3.info.length)
duration.py
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:

oop.py
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):
        ...
oop.py
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 ttkttk widgets for native styling.
  • Add pygame.mixer.music.set_endeventpygame.mixer.music.set_endevent to 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 coffee

Was this page helpful?

Let us know how we did