Tkinter Drag and Drop: Widgets, Canvas, Lists and Files (tkinterdnd2)

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…UseInstall
Move widgets around a windowbind() + place()Nothing
Drag shapes or images on a CanvasCanvas bindings + move()Nothing
Reorder a Listbox or Treeviewnearest() / identify_row() + move()Nothing
Drop files from the desktop into your appdrop_target_register(DND_FILES)pip install tkinterdnd2
Drag files or text out of your appdrag_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()
Python Tkinter drag and drop widgets: three sticky-note labels, one dragged to the bottom right of the window
“Buy milk” after being dragged to the bottom right.

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()
Python Tkinter canvas drag and drop: two coloured circles dragged into a basket drop zone with the counter showing In basket: 2
Two fruits dropped in the basket; the counter updates on release.

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()
Python Tkinter Listbox drag to reorder: the Publish item dragged from the bottom to the second position
“5. Publish” dragged up to second place.

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()
Python Tkinter Treeview drag rows: the Launch row dragged to the top of the table
The “Launch” row dragged to the top.

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()
Python Tkinter drag and drop files with tkinterdnd2: a drop zone label and a list showing Quarterly report.pdf, photo.png and notes.txt with their sizes
After dropping three files, including one with a space in its name.
  • <<DropEnter>> and <<DropLeave>> highlight the zone while files hover over it, so users know where to drop.
  • <<Drop>> receives the file paths in event.data.
  • Each handler returns event.action; tkinterdnd2’s documentation says drop handlers should return an action (such as COPY).

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=&qu