DevelopmentOctober 27, 20254 min read

Paint App using Python and Tkinter

Build a paint app with Python and Tkinter — freehand drawing, color and brush size pickers, eraser, and canvas clearing in 80 lines.

Galvan

Galvan

Founder & Creator

Introduction

A paint app is the most fun way to learn event-driven drawing: the mouse moves, the app responds, lines appear. Tkinter's Canvas widget does the heavy lifting — you bind three mouse events and a drawing app materializes. This build includes freehand drawing, a color picker, adjustable brush size, an eraser, and one-click clear, in about 80 lines.

It is the desktop cousin of the photo booth: both turn input devices into visuals, one via camera, one via mouse.

Features

  • Freehand drawing — smooth lines that follow the mouse.
  • Color picker — any color via the built-in chooser.
  • Brush size slider — 1 to 30 pixels.
  • Eraser mode — draws in the canvas background color.
  • Clear canvas — instant reset.
  • Prerequisites

  • Python 3.8+ with Tkinter (bundled on Windows/macOS; sudo apt install python3-tk on Ubuntu).
  • Step 1: Create the Script

    Save as paint_app.py:

    code
    import tkinter as tk
    from tkinter import colorchooser
    
    
    class PaintApp:
        def __init__(self, root):
            self.root = root
            self.root.title("Paint")
            self.color = "#10b981"
            self.brush_size = 4
            self.erasing = False
            self.last_x = self.last_y = None
            self.BG = "#ffffff"
    
            toolbar = tk.Frame(root, bg="#f3f4f6")
            toolbar.pack(fill="x")
    
            tk.Button(toolbar, text="🎨 Color", command=self.pick_color).pack(side="left", padx=4, pady=4)
            self.color_swatch = tk.Label(toolbar, bg=self.color, width=3, relief="solid")
            self.color_swatch.pack(side="left", padx=2)
    
            tk.Button(toolbar, text="🧽 Eraser", command=self.toggle_eraser, relief="raised").pack(side="left", padx=4)
            self.eraser_btn = toolbar.winfo_children()[-1]
    
            tk.Scale(toolbar, from_=1, to=30, orient="horizontal",
                     command=self.set_size, length=140).pack(side="left", padx=8)
            tk.Button(toolbar, text="🗑 Clear", command=self.clear).pack(side="right", padx=4)
    
            self.canvas = tk.Canvas(root, bg=self.BG, cursor="pencil")
            self.canvas.pack(fill="both", expand=True)
    
            self.canvas.bind("<B1-Motion>", self.draw)
            self.canvas.bind("<ButtonRelease-1>", self.reset)
    
        def pick_color(self):
            chosen = colorchooser.askcolor(self.color)
            if chosen[1]:
                self.color = chosen[1]
                self.color_swatch.config(bg=self.color)
                self.erasing = False
                self.eraser_btn.config(relief="raised")
    
        def toggle_eraser(self):
            self.erasing = not self.erasing
            self.eraser_btn.config(relief="sunken" if self.erasing else "raised")
    
        def set_size(self, value):
            self.brush_size = int(value)
    
        def draw(self, event):
            color = self.BG if self.erasing else self.color
            if self.last_x is not None:
                self.canvas.create_line(
                    self.last_x, self.last_y, event.x, event.y,
                    fill=color, width=self.brush_size,
                    capstyle="round", smooth=True,
                )
            self.last_x, self.last_y = event.x, event.y
    
        def reset(self, event):
            self.last_x = self.last_y = None
    
        def clear(self):
            self.canvas.delete("all")
    
    
    if __name__ == "__main__":
        root = tk.Tk()
        PaintApp(root)
        root.mainloop()

    Step 2: Run the App

    code
    python paint_app.py

    Draw freely, switch colors, resize the brush, erase mistakes — the full paint loop in under a hundred lines.

    How It Works

    Drawing is three event bindings. <B1-Motion> fires continuously while the left button is held; each firing draws a short line segment from the *previous* mouse position to the current one. <ButtonRelease-1> fires on mouse-up and clears the stored previous position — without it, the next stroke would start with a long stray line from where the last one ended. That reset handler is the difference between a paint app and a bug.

    The Canvas stores every segment as a line object — which is both the strength and the ceiling of this approach. Strength: each segment is a real widget you could manipulate. Ceiling: a long drawing session accumulates thousands of objects and slows down; a real paint program would draw into an offscreen image instead. For a utility app, the object model is perfect.

    The eraser is honest about what erasing means here: it draws in the background color. On a plain white canvas that's indistinguishable from true erasing — and one line of code instead of compositing logic.

    smooth=True with capstyle="round" turns the polyline segments into what looks like one continuous stroke — free smoothing from Tkinter's line renderer.

    Common Errors & Fixes

  • Stray lines between strokes — the release handler isn't resetting last_x/last_y, or you bound motion without the release binding.
  • Dots don't appear on single click — a click without motion never fires <B1-Motion>; draw a tiny circle on <Button-1> if dot-painting matters.
  • Eraser leaves colored trails — you forgot to use self.BG as the draw color when self.erasing is true.
  • Window resize clears the drawing — Canvas contents aren't preserved across resizes by default; that's the object-model ceiling — live with it or move to an image-based canvas.
  • Key Concepts

  • Mouse event bindings — B1-Motion, ButtonRelease, and the state between them.
  • Segment chaining — previous-to-current lines build freehand strokes.
  • Canvas objects — every drawn thing is a manageable widget.
  • Eraser-as-painter — background-color drawing as the simple eraser model.
  • What to Try Next

  • Add shape tools — click-drag rectangles and ovals with create_rectangle.
  • Add an undo stack — track canvas item ids per stroke and delete the last batch.
  • Add save to PNG — postscript export via canvas.postscript() plus Pillow conversion.
  • Combine with the digital clock for a drawing app with a live toolbar clock.
  • FAQ

    Why does drawing get slow after a while?

    Every segment is a Canvas object; thousands accumulate. Periodically flattening into an image (Pillow) or restarting the canvas keeps it snappy.

    Can I use this on a touchscreen?

    Yes — Tkinter maps touch to mouse events on most platforms, so finger painting works out of the box.

    How do I make the eraser size independent?

    Use a larger width when self.erasing is true — e.g. self.brush_size * 2 in the draw call.