Tech With Galvan
Back to Articles
Development10/27/2025 • 4 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:

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

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.