Drag and drop in Tkinter means one of two things, and they need different tools. Moving things inside your window (widgets, Canvas shapes, list rows) needs only mouse events: <ButtonPress-1>, <B1-Motion> and <ButtonRelease-1>. Dropping files from Explorer or Finder (or dragging data out to other programs) needs the tkinterdnd2 package, because plain Tkinter cannot receive drops from the operating system. This guide covers both, with complete programs and screenshots.
Tested with Python 3.12.5, Tk 8.6.13, tkinterdnd2 0.6.3 (tkdnd 2.10.2) on Windows 11. The screenshots were taken after sending the same mouse events a real drag produces (event_generate()); for the file drops, the drop handler received real file paths in the exact format tkdnd delivers. For the full reference, see tkinter.dnd in the Python documentation.
| You want to… | Use | Install |
|---|---|---|
| Move widgets around a window | bind() + place() | Nothing |
| Drag shapes or images on a Canvas | Canvas bindings + move() | Nothing |
| Reorder a Listbox or Treeview | nearest() / identify_row() + move() | Nothing |
| Drop files from the desktop into your app | drop_target_register(DND_FILES) | pip install tkinterdnd2 |
| Drag files or text out of your app | drag_source_register() + <<DragInitCmd>> | pip install tkinterdnd2 |
Part 1: drag and drop inside a Tkinter window
Drag widgets with the mouse
Remember where the widget was grabbed on <ButtonPress-1>, then move it with place() on every <B1-Motion>. Clamping the position keeps it inside the window, and lift() brings the dragged widget to the front:
import tkinter as tk
root = tk.Tk()
root.title("Drag the notes")
root.geometry("520x340")
root.configure(bg="#eef2f6")
def start_drag(event):
widget = event.widget
widget._offset = (event.x, event.y) # where inside the widget it was grabbed
widget.lift() # bring it to the front
def on_drag(event):
widget = event.widget
dx, dy = widget._offset
x = widget.winfo_x() + event.x - dx
y = widget.winfo_y() + event.y - dy
# keep the widget inside the window
x = max(0, min(x, root.winfo_width() - widget.winfo_width()))
y = max(0, min(y, root.winfo_height() - widget.winfo_height()))
widget.place(x=x, y=y)
def make_draggable(widget):
widget.bind("<ButtonPress-1>", start_drag)
widget.bind("<B1-Motion>", on_drag)
notes = []
for text, color, x, y in [("Buy milk", "#fff59d", 20, 20),
("Call Anna", "#b3e5fc", 200, 40),
("Finish report", "#c8e6c9", 60, 170)]:
note = tk.Label(root, text=text, bg=color, font=("Segoe UI", 12), width=12, height=3,
relief="raised", bd=1, cursor="fleur")
note.place(x=x, y=y)
make_draggable(note)
notes.append(note)
root.mainloop()
make_draggable() works for any widget: Labels, Buttons, Frames or images. It uses place(), so don’t mix it with pack() or grid() for the same widgets.
Drag items on a Canvas (with a drop zone)
On a Canvas you move items, not widgets. find_overlapping() finds the item under the mouse, move() shifts it by the mouse movement, and on release find_enclosed() checks which items are fully inside the drop zone:
import tkinter as tk
root = tk.Tk()
root.title("Drag the fruit into the basket")
canvas = tk.Canvas(root, width=560, height=340, bg="white", highlightthickness=0)
canvas.pack()
basket = canvas.create_rectangle(350, 170, 540, 325, fill="#f5e6c8", outline="#a1887f", width=3)
canvas.create_text(445, 310, text="Basket", font=("Segoe UI", 12, "bold"), fill="#6d4c41")
counter = canvas.create_text(20, 20, anchor="nw", text="In basket: 0", font=("Segoe UI", 12))
for i, color in enumerate(["#e53935", "#fb8c00", "#7cb342", "#8e24aa"]):
x = 60 + i * 80
canvas.create_oval(x - 25, 60, x + 25, 110, fill=color, outline="", tags="fruit")
drag = {"item": None, "x": 0, "y": 0}
def on_press(event):
# the top-most fruit under the mouse, if any
for item in reversed(canvas.find_overlapping(event.x, event.y, event.x, event.y)):
if "fruit" in canvas.gettags(item):
drag.update(item=item, x=event.x, y=event.y)
canvas.tag_raise(item)
return
def on_motion(event):
if drag["item"]:
canvas.move(drag["item"], event.x - drag["x"], event.y - drag["y"])
drag["x"], drag["y"] = event.x, event.y
def on_release(event):
drag["item"] = None
inside = [i for i in canvas.find_enclosed(*canvas.coords(basket)) if "fruit" in canvas.gettags(i)]
canvas.itemconfig(counter, text=f"In basket: {len(inside)}")
canvas.bind("<ButtonPress-1>", on_press)
canvas.bind("<B1-Motion>", on_motion)
canvas.bind("<ButtonRelease-1>", on_release)
root.mainloop()
The same pattern works for images: use create_image() with a tags="fruit" argument instead of create_oval().
Reorder a Listbox by dragging
nearest(y) tells you which row is under the mouse. When it changes, move the dragged text to the new position:
import tkinter as tk
root = tk.Tk()
root.title("Drag to reorder")
tasks = tk.Listbox(root, font=("Segoe UI", 12), height=6, width=28, activestyle="none",
selectbackground="#306998")
for task in ["1. Write outline", "2. Draft chapter", "3. Add screenshots",
"4. Proofread", "5. Publish"]:
tasks.insert("end", task)
tasks.pack(padx=12, pady=12)
def on_press(event):
tasks._drag_index = tasks.nearest(event.y)
def on_motion(event):
new = tasks.nearest(event.y)
old = tasks._drag_index
if new != old:
text = tasks.get(old)
tasks.delete(old)
tasks.insert(new, text)
tasks.selection_clear(0, "end")
tasks.selection_set(new)
tasks._drag_index = new
tasks.bind("<ButtonPress-1>", on_press)
tasks.bind("<B1-Motion>", on_motion)
root.mainloop()
Reorder Treeview rows by dragging
For a Treeview, identify_row(y) finds the row under the mouse and move() changes its position. add="+" keeps Treeview’s own click handling (selection) working (more on tables in Tkinter table):
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Drag rows to reorder")
tree = ttk.Treeview(root, columns=("task", "owner"), show="headings", height=6)
tree.heading("task", text="Task")
tree.heading("owner", text="Owner")
for row in [("Design logo", "Emma"), ("Write copy", "Liam"), ("Build page", "Ava"),
("Test on mobile", "Noah"), ("Launch", "Mia")]:
tree.insert("", "end", values=row)
tree.pack(padx=12, pady=12)
def on_press(event):
tree._drag_item = tree.identify_row(event.y)
def on_motion(event):
item = tree._drag_item
target = tree.identify_row(event.y)
if item and target and target != item:
tree.move(item, "", tree.index(target))
tree.selection_set(item)
tree.bind("<ButtonPress-1>", on_press, add="+")
tree.bind("<B1-Motion>", on_motion, add="+")
root.mainloop()
Part 2: drag and drop files with tkinterdnd2
To accept files dragged from Explorer (Windows), Finder (macOS) or a Linux file manager, install tkinterdnd2. It wraps the tkdnd Tcl extension and ships the binaries for Windows, macOS and Linux, so there is nothing else to set up:
pip install tkinterdnd2
Two changes turn a normal Tkinter program into a drop target: create the window with TkinterDnD.Tk() instead of tk.Tk(), and call drop_target_register(DND_FILES) on the widget that should accept files.
A file drop zone
import os
import tkinter as tk
from tkinterdnd2 import TkinterDnD, DND_FILES
root = TkinterDnD.Tk() # instead of tk.Tk()
root.title("Drop files here")
zone = tk.Label(root, text="Drag files from Explorer or Finder\nand drop them here",
width=46, height=5, bg="#f4f7fb", relief="ridge", bd=2, font=("Segoe UI", 11))
zone.pack(padx=16, pady=(16, 8))
dropped = tk.Listbox(root, width=58, height=6, font=("Segoe UI", 10))
dropped.pack(padx=16, pady=(0, 16))
def on_enter(event):
zone.config(bg="#d7ecff") # highlight while files hover over the zone
return event.action
def on_leave(event):
zone.config(bg="#f4f7fb")
return event.action
def on_drop(event):
zone.config(bg="#f4f7fb")
for path in root.tk.splitlist(event.data): # handles names with spaces
size = os.path.getsize(path) if os.path.isfile(path) else 0
dropped.insert("end", f"{os.path.basename(path)} ({size:,} bytes)")
return event.action
zone.drop_target_register(DND_FILES)
zone.dnd_bind("<<DropEnter>>", on_enter)
zone.dnd_bind("<<DropLeave>>", on_leave)
zone.dnd_bind("<<Drop>>", on_drop)
root.mainloop()
<<DropEnter>>and<<DropLeave>>highlight the zone while files hover over it, so users know where to drop.<<Drop>>receives the file paths inevent.data.- Each handler returns
event.action; tkinterdnd2’s documentation says drop handlers should return an action (such asCOPY).
event.data is a Tcl list: always use splitlist()
event.data is one string. Paths that contain spaces are wrapped in braces, so split() breaks them apart; tk.splitlist() parses it correctly:
import tkinter as tk
root = tk.Tk()
root.withdraw()
# what event.data looks like when two files are dropped on Windows
data = "{C:/Users/emma/My Documents/Quarterly report.pdf} C:/Users/emma/photo.png"
print(data.split()) # wrong: breaks the name with spaces
print(root.tk.splitlist(data)) # right: one item per file
root.destroy()
Output:
['{C:/Users/emma/My', 'Documents/Quarterly', 'report.pdf}', 'C:/Users/emma/photo.png']
('C:/Users/emma/My Documents/Quarterly report.pdf', 'C:/Users/emma/photo.png')
Drop files onto a Treeview (DND_FILES example)
Every widget gets the drag and drop methods once tkinterdnd2 is imported, including ttk.Treeview. This table accepts only data files and reports what it skipped:
import os
import tkinter as tk
from tkinter import ttk
from tkinterdnd2 import TkinterDnD, DND_FILES
ALLOWED = {".csv", ".txt", ".xlsx"}
root = TkinterDnD.Tk()
root.title("Drop data files on the table")
tree = ttk.Treeview(root, columns=("name", "type", "size"), show="headings", height=6)
for col, width, anchor in [("name", 220, "w"), ("type", 70, "w"), ("size", 100, "e")]:
tree.heading(col, text=col.title())
tree.column(col, width=width, anchor=anchor)
tree.pack(padx=12, pady=(12, 4))
status = ttk.Label(root, text="Drop .csv, .txt or .xlsx files on the table")
status.pack(anchor="w", padx=12, pady=(0, 12))
def on_drop(event):
added, skipped = 0, []
for path in tree.tk.splitlist(event.data):
ext = os.path.splitext(path)[1].lower()
if os.path.isfile(path) and ext in ALLOWED:
tree.insert("", "end", values=(os.path.basename(path), ext[1:].upper(),
f"{os.path.getsize(path) / 1024:.1f} KB"))
added += 1
else:
skipped.append(os.path.basename(path))
status.config(text=f"Added {added} file(s). Skipped: {', '.join(skipped) or 'none'}")
return event.action
tree.drop_target_register(DND_FILES) # works on ttk widgets too
tree.dnd_bind("<<Drop>>", on_drop)
root.mainloop()
Accept both files and text
Register several types and bind <<Drop:DND_Files>> and <<Drop:DND_Text>> separately, so each handler knows what it received (text comes from browsers, editors and other apps):
import tkinter as tk
from tkinterdnd2 import TkinterDnD, DND_FILES, DND_TEXT
root = TkinterDnD.Tk()
root.title("Drop files or text")
box = tk.Text(root, width=50, height=8)
box.pack(padx=10, pady=10)
def drop_files(event):
for path in box.tk.splitlist(event.data):
box.insert("end", f"File: {path}\n")
return event.action
def drop_text(event):
box.insert("end", f"Text: {event.data}\n")
return event.action
box.drop_target_register(DND_FILES, DND_TEXT)
box.dnd_bind("<<Drop:DND_Files>>", drop_files) # one handler per data type
box.dnd_bind("<<Drop:DND_Text>>", drop_text)
root.mainloop()
Output (handlers called with sample file and text data):
copy copy
File: C:/My Files/a b.txt
File: C:/data/c.csv
Text: Hello from another app
Drag files out of your app with <>
tkinterdnd2 also works the other way: users can drag items from your window into Explorer, Finder, an email or another program. Register the widget as a drag source and return a tuple of (action, type, data) from the <<DragInitCmd>> handler:
import tkinter as tk
from tkinterdnd2 import TkinterDnD, DND_FILES, COPY
root = TkinterDnD.Tk()
root.title("Drag files out of this window")
files = tk.Listbox(root, width=60, height=5, selectmode="extended")
files.pack(padx=10, pady=10)
PATHS = [r"C:\Reports\Quarterly report.pdf", r"C:\Reports\notes.txt"]
for path in PATHS: # absolute paths of real files
files.insert("end", path)
def drag_init(event):
selected = tuple(files.get(i) for i in files.curselection())
if not selected:
return None # nothing selected: no drag
return (COPY, DND_FILES, selected) # (action, type, data)
def drag_end(event):
print("Drag finished with action:", event.action)
files.drag_source_register(1, DND_FILES) # left mouse button starts the drag
files.dnd_bind("<<DragInitCmd>>", drag_init)
files.dnd_bind("<<DragEndCmd>>", drag_end)
root.mainloop()
Output (drag_init() called directly, before and after selecting two rows):
None
copy DND_Files ['Quarterly report.pdf', 'notes.txt']
The first argument of drag_source_register() is the mouse button that starts the drag (1 = left, 3 = right). Returning None when nothing is selected cancels the drag. <<DragEndCmd>> runs when the drop finishes, with the action the target accepted.
Using tkinterdnd2 with CustomTkinter
CustomTkinter’s CTk window is a subclass of tk.Tk, so add TkinterDnD.DnDWrapper as a second base class and load tkdnd with TkinterDnD.require():
import customtkinter as ctk
from tkinterdnd2 import TkinterDnD, DND_FILES
class App(ctk.CTk, TkinterDnD.DnDWrapper):
def __init__(self):
super().__init__()
self.TkdndVersion = TkinterDnD.require(self) # load tkdnd into this window
app = App()
app.title("CustomTkinter + tkinterdnd2")
label = ctk.CTkLabel(app, text="Drop files here", width=300, height=120)
label.pack(padx=20, pady=20)
def on_drop(event):
label.configure(text="\n".join(app.tk.splitlist(event.data)))
return event.action
label.drop_target_register(DND_FILES)
label.dnd_bind("<<Drop>>", on_drop)
app.mainloop()
Output (the program run with CustomTkinter 6.0.0, closed before the main loop):
tkdnd 2.10.2 loaded; drop target registered
tkinterdnd2 events and the event object
| Event | When it fires |
|---|---|
<<DropEnter>> | Dragged data enters the widget |
<<DropPosition>> | The mouse moves over the widget while dragging |
<<DropLeave>> | Dragged data leaves without dropping |
<<Drop>> | Data is dropped (any registered type) |
<<Drop:DND_Files>>, <<Drop:DND_Text>> | Data of that type is dropped |
<<DragInitCmd>> | A drag starts from a drag source; return (action, type, data) |
<<DragEndCmd>> | A drag from your widget has finished |
Useful attributes of the event object: data (the dropped data), action (copy, move, link…), widget, x_root/y_root (mouse position on screen), types and modifiers (keys held down).
Common errors
Using tk.Tk() instead of TkinterDnD.Tk() means tkdnd is never loaded, and registering a drop target fails:
import tkinter as tk
from tkinterdnd2 import DND_FILES
root = tk.Tk() # plain Tk: tkdnd is not loaded
label = tk.Label(root, text="Drop here")
try:
label.drop_target_register(DND_FILES)
except tk.TclError as err:
print("TclError:", err)
root.destroy()
Output:
TclError: invalid command name "tkdnd::drop_target"
- Paths broken at spaces: use
widget.tk.splitlist(event.data), notevent.data.split(). ModuleNotFoundError: No module named 'tkinterdnd2': install it into the same Python you run:python -m pip install tkinterdnd2.- The widget jumps when you start dragging: store the grab offset on
<ButtonPress-1>and subtract it, as in the first example. - What about
tkinter.dnd? The standard library has a small drag and drop module for use inside one application, but its documentation describes it as experimental and due to be deprecated. Plain mouse bindings (Part 1) or tkinterdnd2 (Part 2) are the better choices.
Frequently asked questions
How do I drag and drop files into a Tkinter window?
Install tkinterdnd2, create the window with TkinterDnD.Tk(), call widget.drop_target_register(DND_FILES) and bind <<Drop>>. The paths are in event.data; split them with widget.tk.splitlist(event.data).
Can Tkinter do drag and drop without extra libraries?
Yes, inside your own window: bind <ButtonPress-1> and <B1-Motion> to move widgets or Canvas items. Receiving files from the desktop needs tkinterdnd2.
How do I use drop_target_register with a Treeview?
The same way as any widget: tree.drop_target_register(DND_FILES) and tree.dnd_bind("<<Drop>>", handler). tkinterdnd2 adds these methods to every Tkinter and ttk widget.
What should the <> handler return?
A tuple of three items: the action (for example COPY), the data type (DND_FILES or DND_TEXT) and the data itself, such as a tuple of file paths. Return None to cancel the drag.
Does tkinterdnd2 work with CustomTkinter?
Yes. Subclass ctk.CTk and TkinterDnD.DnDWrapper, and call TkinterDnD.require(self) in __init__, as shown above.
Build on this with these Tkinter guides:
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn