Windows Explorer’s built-in copy dialog is fine for the occasional file, but it falls apart for anything serious โ no reliable retry on network hiccups, no proper multithreading, and no easy way to verify a large transfer actually completed correctly. I wanted something better for moving large batches of files between drives and network shares, so I built my own.
Why not just use Robocopy directly?
Robocopy is genuinely excellent โ it’s Microsoft’s own robust copy utility, built into every recent Windows version, and it handles retries, multithreading, and long paths far better than Explorer ever will. The problem is it’s a command-line tool with a syntax that’s easy to get wrong, and it gives you no visual feedback beyond a stream of text scrolling past.
The plan was to keep Robocopy doing the actual heavy lifting โ since there’s no point reinventing a reliable copy engine โ and build a proper GUI on top of it that shows real progress, lets you pick source and destination visually, and verifies the result afterwards.
Choosing the stack
Python for the application logic โ quick to iterate, and calling out to Robocopy as a subprocess is trivial.
CustomTkinter for the UI โ a modern-looking wrapper around Python’s built-in Tkinter. Standard Tkinter looks dated out of the box; CustomTkinter gives rounded corners, proper dark mode, and a much more polished feel without needing a heavier framework like PyQt.
PyInstaller to package the finished app into a single standalone .exe โ so it runs on any Windows machine without needing Python installed.
The dual-pane layout
The interface mirrors the classic dual-pane file manager style โ source on the left, destination on the right, with the transfer controls in between. Each pane has its own folder browser, and you can navigate independently on both sides before starting a copy.
import customtkinter as ctk
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
class FileCopyApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("File Copy Tool")
self.geometry("1000x600")
self.source_pane = FilePane(self, label="Source")
self.source_pane.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
self.dest_pane = FilePane(self, label="Destination")
self.dest_pane.grid(row=0, column=2, sticky="nsew", padx=10, pady=10)
self.controls = ctk.CTkFrame(self)
self.controls.grid(row=0, column=1, sticky="ns", padx=5)
Each FilePane is its own widget class handling its own folder tree, current path label, and selection state โ keeping the two sides completely decoupled until a copy operation ties them together.
Wrapping Robocopy
The core of the tool is a wrapper function that builds a Robocopy command from the UI’s selected source, destination, and options, then runs it as a subprocess and parses its output line by line to drive a progress bar.
import subprocess
import threading
def run_robocopy(source, destination, progress_callback):
cmd = [
"robocopy",
source,
destination,
"/E", # copy subdirectories, including empty ones
"/MT:16", # multithreaded, 16 threads
"/R:3", # retry 3 times on failed copies
"/W:5", # wait 5 seconds between retries
"/NFL", "/NDL", # no file/dir list in output (cleaner parsing)
"/BYTES", # show sizes in bytes for consistent parsing
]
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
for line in process.stdout:
progress_callback(line.strip())
process.wait()
return process.returncode
Running this in a background thread (via Python’s threading module) keeps the UI responsive during large transfers โ without it, CustomTkinter’s main loop would freeze for the entire duration of the copy.
/MT:16 is doing a lot of the real work here โ Robocopy’s multithreaded mode dramatically speeds up transfers involving many small files, since it can copy several files in parallel rather than one at a time. For a handful of huge files it matters less, but for folders with thousands of small files it’s the difference between minutes and seconds.
Progress tracking
Robocopy’s console output includes percentage-complete lines for each file it copies. The wrapper parses these out with a regex and updates a CTkProgressBar in real time:
import re
progress_pattern = re.compile(r"(\d+)%")
def parse_progress_line(line):
match = progress_pattern.search(line)
if match:
return int(match.group(1))
return None
Combined with a running count of files copied versus total files (calculated up front by walking the source directory), the UI shows both an overall percentage and a “X of Y files” counter.
Size verification
Once Robocopy reports it’s finished, the tool doesn’t just trust the exit code โ it independently walks both the source and destination directories, sums the total byte size of each, and compares them.
import os
def get_directory_size(path):
total = 0
for dirpath, _, filenames in os.walk(path):
for f in filenames:
fp = os.path.join(dirpath, f)
if os.path.exists(fp):
total += os.path.getsize(fp)
return total
def verify_copy(source, destination):
source_size = get_directory_size(source)
dest_size = get_directory_size(destination)
return source_size == dest_size, source_size, dest_size
If the sizes don’t match, the app flags it clearly rather than silently reporting success โ this catches the rare case of a file that failed to copy but didn’t trigger a Robocopy error (for example, a file that was open and locked by another process at the exact moment of the transfer).
Packaging with PyInstaller
To make the tool usable without a Python environment installed, it’s bundled into a single executable:
pyinstaller --onefile --windowed --icon=app_icon.ico --name="FileCopyTool" main.py
--onefilebundles everything into a single.exerather than a folder of dependencies--windowedsuppresses the console window, since this is a GUI app--iconsets a proper taskbar/executable icon rather than the default Python one
The resulting executable is a few dozen megabytes (Python’s runtime and CustomTkinter’s dependencies add up) but runs standalone on any Windows 10/11 machine.
Result
The finished tool handles exactly the workflow I wanted: pick a source and destination visually, kick off a multithreaded Robocopy transfer with sane defaults, watch real progress rather than a spinning cursor, and get a clear confirmation that the copy actually completed correctly โ all wrapped in a single executable I can drop on any machine without installing anything.