Basic Text Editor
Abstract
Section titled “Abstract”A text editor is the most concentrated GUI project a Python developer can build. You touch every part of Tkinter — menus, toolbars, the Text widget, dialog windows, keyboard shortcuts, clipboard integration, status bars, and asking-to-save logic. In this tutorial you will build a working editor with new/open/save/save-as, undo/redo, find, font selection, and word-wrap toggle. Then we will discuss the natural next steps — syntax highlighting, tabs, and find-and-replace.
You will learn:
- How to lay out a real application with
pack+ frames. - How a Tkinter menubar, toolbar, and status bar fit together.
- How to wire keyboard shortcuts to commands.
- How to track unsaved changes and prompt the user before discarding them.
- How to use the
Textwidget’s built-in undo/redo.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Tkinter (ships with Python; Linux:
sudo apt install python3-tk). - Comfort with classes — this project deserves OO from the start.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create folder
basic-text-editor. - Inside, create
basictexteditor.py.
Run it
Section titled “Run it”python basictexteditor.pyA window appears with File, Edit, Format menus, a toolbar, the editing area, and a status bar at the bottom.
Class Skeleton
Section titled “Class Skeleton”import tkinter as tk
from tkinter import filedialog, messagebox, font, ttk
class BasicTextEditor:
def __init__(self):
self.root = tk.Tk()
self.root.title("Untitled — Basic Text Editor")
self.root.geometry("900x600")
self.current_file = None
self.modified = False
self._build_menu()
self._build_toolbar()
self._build_text_area()
self._build_status_bar()
self._bind_shortcuts()
def run(self):
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.root.mainloop()Each _build_* helper does one thing. The constructor is now a high-level recipe.
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. The text area
Section titled “1. The text area”def _build_text_area(self):
frame = tk.Frame(self.root)
frame.pack(fill="both", expand=True)
self.text = tk.Text(frame, undo=True, wrap="word", font=("Consolas", 11))
vsb = ttk.Scrollbar(frame, orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.text.pack(side="left", fill="both", expand=True)
self.text.bind("<<Modified>>", self._on_modified)undo=Trueenables built-in undo/redo.wrap="word"wraps at word boundaries (toggleable later).<<Modified>>fires the first time the text content changes afteredit_modified(False)— we use it to track unsaved changes.
2. The menubar
Section titled “2. The menubar”def _build_menu(self):
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="New", command=self.new_file, accelerator="Ctrl+N")
file_menu.add_command(label="Open...", command=self.open_file, accelerator="Ctrl+O")
file_menu.add_command(label="Save", command=self.save_file, accelerator="Ctrl+S")
file_menu.add_command(label="Save As...",command=self.save_file_as, accelerator="Ctrl+Shift+S")
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self.on_close)
menubar.add_cascade(label="File", menu=file_menu)
edit_menu = tk.Menu(menubar, tearoff=0)
edit_menu.add_command(label="Undo", command=self.text.edit_undo, accelerator="Ctrl+Z")
edit_menu.add_command(label="Redo", command=self.text.edit_redo, accelerator="Ctrl+Y")
edit_menu.add_separator()
edit_menu.add_command(label="Cut", command=lambda: self.text.event_generate("<<Cut>>"))
edit_menu.add_command(label="Copy", command=lambda: self.text.event_generate("<<Copy>>"))
edit_menu.add_command(label="Paste", command=lambda: self.text.event_generate("<<Paste>>"))
edit_menu.add_command(label="Select All", command=self._select_all)
edit_menu.add_command(label="Find...", command=self.show_find, accelerator="Ctrl+F")
menubar.add_cascade(label="Edit", menu=edit_menu)
format_menu = tk.Menu(menubar, tearoff=0)
format_menu.add_command(label="Font...", command=self.show_font_dialog)
self.wrap_var = tk.BooleanVar(value=True)
format_menu.add_checkbutton(label="Word Wrap", variable=self.wrap_var,
command=self._toggle_wrap)
menubar.add_cascade(label="Format", menu=format_menu)accelerator="Ctrl+N"only shows the shortcut next to the menu label. You bind it separately (see_bind_shortcuts).tearoff=0disables the legacy Tk feature of dragging menus into floating windows.
3. File operations
Section titled “3. File operations”def new_file(self):
if not self._confirm_discard(): return
self.text.delete("1.0", "end")
self.current_file = None
self.modified = False
self.text.edit_modified(False)
self._set_title()
def open_file(self):
if not self._confirm_discard(): return
path = filedialog.askopenfilename(filetypes=[("Text", "*.txt"),
("Python", "*.py"),
("All files", "*.*")])
if not path: return
try:
with open(path, encoding="utf-8") as f:
data = f.read()
except Exception as e:
messagebox.showerror("Open failed", str(e))
return
self.text.delete("1.0", "end")
self.text.insert("1.0", data)
self.current_file = path
self.modified = False
self.text.edit_modified(False)
self._set_title()
def save_file(self):
if self.current_file is None:
return self.save_file_as()
try:
with open(self.current_file, "w", encoding="utf-8") as f:
f.write(self.text.get("1.0", "end-1c"))
self.modified = False
self.text.edit_modified(False)
self._set_title()
except Exception as e:
messagebox.showerror("Save failed", str(e))
def save_file_as(self):
path = filedialog.asksaveasfilename(defaultextension=".txt",
filetypes=[("Text", "*.txt"), ("Python", "*.py"), ("All", "*.*")])
if path:
self.current_file = path
self.save_file()"end-1c"strips the trailing newline Tkinter implicitly appends._confirm_discardasks the user if there are unsaved changes before discarding them — covered next.
4. Tracking unsaved changes
Section titled “4. Tracking unsaved changes”def _on_modified(self, _event):
if self.text.edit_modified():
self.modified = True
self._set_title()
def _confirm_discard(self) -> bool:
if not self.modified:
return True
ans = messagebox.askyesnocancel("Unsaved changes",
"You have unsaved changes. Save now?")
if ans is None: return False
if ans:
self.save_file()
return not self.modified
return True
def _set_title(self):
name = self.current_file or "Untitled"
mark = "*" if self.modified else ""
self.root.title(f"{mark}{name} — Basic Text Editor")askyesnocancelreturnsTrue/False/None— three outcomes, three behaviors.- The window title shows
*for unsaved files — the universal convention.
5. Keyboard shortcuts
Section titled “5. Keyboard shortcuts”def _bind_shortcuts(self):
self.root.bind_all("<Control-n>", lambda e: self.new_file())
self.root.bind_all("<Control-o>", lambda e: self.open_file())
self.root.bind_all("<Control-s>", lambda e: self.save_file())
self.root.bind_all("<Control-Shift-S>", lambda e: self.save_file_as())
self.root.bind_all("<Control-f>", lambda e: self.show_find())
self.root.bind_all("<Control-a>", lambda e: self._select_all())bind_all ensures the shortcut works no matter which widget has focus.
6. Find dialog
Section titled “6. Find dialog”def show_find(self):
win = tk.Toplevel(self.root)
win.title("Find")
tk.Label(win, text="Find:").pack(side="left")
entry = tk.Entry(win)
entry.pack(side="left", padx=4)
def do_find():
self.text.tag_remove("found", "1.0", "end")
target = entry.get()
if not target: return
start = "1.0"
while True:
idx = self.text.search(target, start, stopindex="end", nocase=True)
if not idx: break
end = f"{idx}+{len(target)}c"
self.text.tag_add("found", idx, end)
start = end
self.text.tag_config("found", background="yellow")
tk.Button(win, text="Find", command=do_find).pack(side="left")A real Toplevel mini-window with an Entry and a Button — the same pattern works for find-and-replace.
7. Font customization
Section titled “7. Font customization”def show_font_dialog(self):
win = tk.Toplevel(self.root); win.title("Font")
families = sorted(font.families())
fam = tk.StringVar(value="Consolas")
size = tk.IntVar(value=11)
ttk.Combobox(win, textvariable=fam, values=families).pack()
tk.Spinbox(win, from_=8, to=72, textvariable=size).pack()
tk.Button(win, text="Apply",
command=lambda: self.text.config(font=(fam.get(), size.get()))).pack()8. Status bar
Section titled “8. Status bar”def _build_status_bar(self):
self.status = tk.Label(self.root, text="Line 1, Col 0", anchor="w", relief="sunken")
self.status.pack(side="bottom", fill="x")
self.text.bind("<KeyRelease>", self._update_status)
self.text.bind("<ButtonRelease>", self._update_status)
def _update_status(self, _e=None):
line, col = self.text.index("insert").split(".")
self.status.config(text=f"Line {line}, Col {col}")9. Closing the window
Section titled “9. Closing the window”def on_close(self):
if self._confirm_discard():
self.root.destroy()Protocol hook ensures the X button also asks about unsaved changes.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| Extra newline on save | Used get("1.0", "end") | Use get("1.0", "end-1c") |
| Ctrl+S does nothing | Bound on self.text, not self.root | Use bind_all |
* never disappears | Forgot edit_modified(False) after save | Reset after every save |
| Save dialog skips on Save vs Save As | Mixed semantics | save_file calls save_file_as when current_file is None |
UnicodeDecodeError opening files | Default encoding | Use encoding="utf-8", catch and report |
Variations to Try
Section titled “Variations to Try”1. Find and Replace
Section titled “1. Find and Replace”Add a second Entry and a “Replace” button that calls text.replace(start, end, new) for each match.
2. Syntax highlighting
Section titled “2. Syntax highlighting”Use Pygments to color Python/JS/etc:
from pygments import lex
from pygments.lexers import get_lexer_for_filename
for tok, val in lex(code, get_lexer_for_filename(path)):
self.text.insert("end", val, str(tok))
self.text.tag_config("Token.Keyword", foreground="blue")3. Multiple tabs
Section titled “3. Multiple tabs”Use ttk.Notebook. Each tab owns its own Text widget and file state.
4. Line numbers
Section titled “4. Line numbers”A side panel that mirrors the main Text and shows numbers. Real implementations use a canvas with re-renders on scroll/edit.
5. Recent files
Section titled “5. Recent files”Keep the last N opened paths in a JSON config; show as the bottom of the File menu.
6. Themes (dark/light)
Section titled “6. Themes (dark/light)”Store palettes in a dict; apply by iterating all widgets and reconfiguring bg/fg.
7. Print support
Section titled “7. Print support”Use os.startfile(path, "print") on Windows or lpr via subprocess on Linux/macOS.
8. Autosave
Section titled “8. Autosave”Every 30 seconds, write to current_file + ".autosave". Offer to restore on next open if it exists and is newer.
9. Markdown preview
Section titled “9. Markdown preview”Pair with a tkhtmlview widget that renders Markdown to HTML side-by-side.
10. Drag and drop
Section titled “10. Drag and drop”tkinterdnd2 lets users drop files onto the window to open them.
Architecture Notes
Section titled “Architecture Notes”A class-based editor avoids global pitfalls and isolates state per window. Add a Tab class once you implement notebook tabs — each tab carries its own Text, current_file, and modified flag.
Keep three responsibilities separate:
- Storage — load/save files, encoding handling.
- UI — menus, toolbar, status bar, key bindings.
- Document — text content, modified flag, undo stack (Tkinter’s
Textalready handles this).
When these blur, every new feature breaks two old ones.
Real-World Applications
Section titled “Real-World Applications”- Internal tooling — log viewers, config editors.
- Educational tools — student note-takers.
- Specialized editors — Markdown, YAML, Python-only editors.
- Onboarding for “real” editors — VS Code, Sublime use the same primitives, just way more of them.
Educational Value
Section titled “Educational Value”- Tkinter widget composition at scale (menus, toolbars, dialogs, status bars).
- Event-driven flow tracking (
<<Modified>>). - Unsaved-changes UX — three-button askyesnocancel.
- Cross-cutting concerns — keyboard shortcuts as a layer over commands.
- Class-based GUI architecture.
Next Steps
Section titled “Next Steps”- Implement find-and-replace.
- Add syntax highlighting with Pygments.
- Add multiple tabs with
ttk.Notebook. - Add line numbers in a side panel.
- Add recent files list.
- Compare with Basic File Explorer — the smaller cousin of this project.
Conclusion
Section titled “Conclusion”You built a real text editor: menus, toolbar, status bar, undo/redo, find, fonts, word wrap, and unsaved-changes handling. The class-based architecture means every future feature has a clear home. From here, every step toward “real” editor functionality is incremental — and every step you skip is one a commercial editor like VS Code already did for you. Find more GUI projects on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading