Seating Arrangement Generator – A Smart Desktop App Built in Python

 

Seating Arrangement Generator – A Smart Desktop App Built in Python

Organizing a seating plan for events, classrooms, examinations, or meetings often becomes more complicated than it seems. Whether you are a teacher preparing seating charts, a manager planning a workshop layout, or someone arranging guests for a family function, ensuring optimal placement while meeting constraints can consume a lot of time. To solve this problem in a simple and effective way, I created a Seating Arrangement Generator, a user-friendly Python desktop application that automates the entire process with just a few clicks.

This app, designed using Python and Tkinter, helps generate optimized seating layouts by allowing users to define rows, columns, fixed seats, and “avoid pairs”—individuals who should not sit near each other. Once the constraints are set, the application instantly generates a clean seating matrix. You can save the arrangement as a CSV file or reload previous lists whenever needed. It’s simple, efficient, and practical for real-world use.


Why Build a Seating Arrangement Tool?

In schools, colleges, corporate trainings, or even small events, seating arrangements are often prepared manually. This leads to:

  • Time consumption

  • Errors and confusion

  • Difficulty handling constraints (like separating talking friends or keeping certain individuals apart)

  • Rewriting arrangements multiple times

With this Python desktop app, all of these issues are eliminated. The tool automates the arrangement process using logical algorithms and gives a correct layout every time.


Key Features of the App

1. Custom Rows and Columns

You can set any number of rows or columns depending on the hall or classroom size. Whether you need a layout for 10 seats or 500 seats, the tool adapts easily.

2. Import Names from CSV

Simply load your student list, employee list, or guest list from a CSV file. No need to enter names manually.

3. Fixed Seat Assignments

If certain individuals must sit in a specific seat (like VIPs or invigilators), you can lock those seats. The generator will place everyone else around them automatically.

4. Avoid Pair Logic

The app supports "avoid pairs," meaning two people who should not sit next to each other—perfect for exam seating or resolving interpersonal conflict in teams.

5. Random or Reshuffled Seating

Each time you click the generate button, the algorithm rearranges individuals to create a fresh layout.

6. Export Final Seating Plan

Save the generated seating chart as a CSV file so it can be printed, shared, or reused.

7. Clean and Simple Interface

The Tkinter GUI is intuitive and suitable even for users with no technical background.


How the App Works

The app takes the following input:

  • Total seats (rows × columns)

  • List of names

  • Fixed seat rules

  • Avoid pair instructions

After that, a Python algorithm:

  1. Randomizes the seating,

  2. Checks for conflicts,

  3. Applies rules,

  4. And generates a valid arrangement.

If the seating violates constraints, the system reshuffles until an optimized layout is produced.


Use Cases

This Seating Arrangement Generator is ideal for:

  • Schools & Colleges: Exam seating, classroom organization

  • Corporate Training: Employee seating during workshops

  • Events & Functions: Guest arrangement, family functions

  • Meetings & Seminars: Pre-planned layouts for better management

  • Competitions: Fair and random seating arrangements


Why This App Is Useful

The challenge of perfect seating planning lies in balancing constraints. Doing it manually is tedious, especially for large groups. This tool saves hours of work and delivers accurate results instantly. Built in Python, it is easily modifiable and can be extended with new features like color-coded charts, distance rules, printable PDFs, and more.


Conclusion

The Seating Arrangement Generator is a practical Python utility designed to handle real-life seating challenges with ease. Whether you're a teacher, organizer, or professional, this app offers the flexibility you need with the simplicity you want. With just a few clicks, you can create smart, balanced, and rule-compliant seating plans—making event and classroom management smoother than ever.

"""

Seating Arrangement Generator

Single-file Python desktop application using Tkinter.


Features:

- Add/remove names manually

- Import names from a CSV file (one name per line or a single column)

- Specify rows x columns for seating layout

- Set fixed seat assignments (Name -> seat index)

- Add avoid-pair constraints (two names who should not sit adjacent)

- Generate random arrangements that respect constraints (tries multiple times)

- Save/export arrangements to CSV

- Preview seating as grid


Usage:

- Save this file as seating_generator.py and run: python seating_generator.py

- Requires: Python 3.x. Tkinter is included with standard Python on most platforms.

- For CSV import/export the builtin csv module is used; pandas is *not* required.


Limitations & notes:

- Adjacency is considered orthogonal (up/down/left/right) in the grid.

- The algorithm attempts to find a valid seating by random shuffles and checking constraints; for difficult constraints or large lists it may fail after max_tries.


"""


import tkinter as tk

from tkinter import ttk, filedialog, messagebox

import random

import csv

import os


MAX_TRIES = 2000


class SeatingGeneratorApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("Seating Arrangement Generator")

        self.geometry("980x640")

        self.resizable(True, True)


        # Data

        self.names = []  # list of strings

        self.fixed = {}  # seat_index -> name

        self.avoid_pairs = set()  # set of frozenset({a,b})


        # UI

        self.create_widgets()


    def create_widgets(self):

        main = ttk.Frame(self, padding=8)

        main.pack(fill=tk.BOTH, expand=True)


        left = ttk.Frame(main)

        left.pack(side=tk.LEFT, fill=tk.Y, padx=(0,8))


        # Name management

        nm_frame = ttk.LabelFrame(left, text="Names")

        nm_frame.pack(fill=tk.Y, expand=False)


        self.name_entry = ttk.Entry(nm_frame, width=25)

        self.name_entry.grid(row=0, column=0, padx=4, pady=4)

        add_btn = ttk.Button(nm_frame, text="Add", command=self.add_name)

        add_btn.grid(row=0, column=1, padx=4, pady=4)


        import_btn = ttk.Button(nm_frame, text="Import CSV", command=self.import_csv)

        import_btn.grid(row=1, column=0, columnspan=2, sticky=tk.EW, padx=4)


        self.names_listbox = tk.Listbox(nm_frame, width=30, height=12, selectmode=tk.SINGLE)

        self.names_listbox.grid(row=2, column=0, columnspan=2, padx=4, pady=6)


        rm_btn = ttk.Button(nm_frame, text="Remove Selected", command=self.remove_selected)

        rm_btn.grid(row=3, column=0, columnspan=2, sticky=tk.EW, padx=4)


        clear_btn = ttk.Button(nm_frame, text="Clear All", command=self.clear_all)

        clear_btn.grid(row=4, column=0, columnspan=2, sticky=tk.EW, padx=4, pady=(4,8))


        # Layout settings

        layout_frame = ttk.LabelFrame(left, text="Layout & Constraints")

        layout_frame.pack(fill=tk.X, expand=False, pady=(8,0))


        ttk.Label(layout_frame, text="Rows:").grid(row=0, column=0, sticky=tk.W, padx=4, pady=4)

        self.rows_var = tk.IntVar(value=3)

        rows_spin = ttk.Spinbox(layout_frame, from_=1, to=50, textvariable=self.rows_var, width=6)

        rows_spin.grid(row=0, column=1, padx=4)


        ttk.Label(layout_frame, text="Cols:").grid(row=0, column=2, sticky=tk.W, padx=4)

        self.cols_var = tk.IntVar(value=4)

        cols_spin = ttk.Spinbox(layout_frame, from_=1, to=50, textvariable=self.cols_var, width=6)

        cols_spin.grid(row=0, column=3, padx=4)


        ttk.Separator(layout_frame, orient=tk.HORIZONTAL).grid(row=1, column=0, columnspan=4, sticky=tk.EW, pady=6)


        # Fixed seat assignment

        ttk.Label(layout_frame, text="Fixed seat (name -> seat idx):").grid(row=2, column=0, columnspan=4, sticky=tk.W, padx=4)

        self.fixed_name_var = tk.StringVar()

        self.fixed_seat_var = tk.IntVar(value=0)

        ttk.Entry(layout_frame, textvariable=self.fixed_name_var, width=14).grid(row=3, column=0, padx=4, pady=4)

        ttk.Entry(layout_frame, textvariable=self.fixed_seat_var, width=6).grid(row=3, column=1, padx=4)

        ttk.Button(layout_frame, text="Set Fixed", command=self.set_fixed).grid(row=3, column=2, padx=4)

        ttk.Button(layout_frame, text="Clear Fixed", command=self.clear_fixed).grid(row=3, column=3, padx=4)


        # Avoid pair

        ttk.Label(layout_frame, text="Avoid pair (name1,name2):").grid(row=4, column=0, columnspan=4, sticky=tk.W, padx=4)

        self.avoid_a_var = tk.StringVar()

        self.avoid_b_var = tk.StringVar()

        ttk.Entry(layout_frame, textvariable=self.avoid_a_var, width=12).grid(row=5, column=0, padx=4, pady=4)

        ttk.Entry(layout_frame, textvariable=self.avoid_b_var, width=12).grid(row=5, column=1, padx=4)

        ttk.Button(layout_frame, text="Add Avoid", command=self.add_avoid).grid(row=5, column=2, padx=4)

        ttk.Button(layout_frame, text="Clear Avoids", command=self.clear_avoids).grid(row=5, column=3, padx=4)


        # Avoids list

        self.avoids_listbox = tk.Listbox(layout_frame, width=30, height=4)

        self.avoids_listbox.grid(row=6, column=0, columnspan=4, padx=4, pady=6)


        # Generate controls

        gen_frame = ttk.LabelFrame(left, text="Generate & Export")

        gen_frame.pack(fill=tk.X, expand=False, pady=8)


        self.max_tries_var = tk.IntVar(value=MAX_TRIES)

        ttk.Label(gen_frame, text="Max tries:").grid(row=0, column=0, padx=4, pady=6)

        ttk.Entry(gen_frame, textvariable=self.max_tries_var, width=8).grid(row=0, column=1, padx=4)


        ttk.Button(gen_frame, text="Generate Arrangement", command=self.generate_arrangement).grid(row=1, column=0, columnspan=2, sticky=tk.EW, padx=4, pady=6)

        ttk.Button(gen_frame, text="Save to CSV", command=self.save_csv).grid(row=2, column=0, columnspan=2, sticky=tk.EW, padx=4)


        # Right side: preview / grid

        right = ttk.Frame(main)

        right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)


        preview_frame = ttk.LabelFrame(right, text="Seating Preview")

        preview_frame.pack(fill=tk.BOTH, expand=True)


        # Canvas for grid

        self.grid_canvas = tk.Canvas(preview_frame, bg="#f8f8f8")

        self.grid_canvas.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)

        self.grid_canvas.bind("<Configure>", self.redraw_grid)


        # textual output

        out_frame = ttk.Frame(right)

        out_frame.pack(fill=tk.X)

        ttk.Label(out_frame, text="Last arrangement (seat_index: name)").pack(anchor=tk.W, padx=6)

        self.output_text = tk.Text(out_frame, height=6)

        self.output_text.pack(fill=tk.X, padx=6, pady=4)


        # status bar

        self.status_var = tk.StringVar(value="Ready")

        status = ttk.Label(self, textvariable=self.status_var, anchor=tk.W)

        status.pack(side=tk.BOTTOM, fill=tk.X)


        # current arrangement

        self.current_arrangement = []


    # ---- Name management ----

    def add_name(self):

        name = self.name_entry.get().strip()

        if not name:

            return

        if name in self.names:

            messagebox.showinfo("Info", f"{name} already in list")

            return

        self.names.append(name)

        self.names_listbox.insert(tk.END, name)

        self.name_entry.delete(0, tk.END)


    def import_csv(self):

        path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv"), ("All files", "*")])

        if not path:

            return

        try:

            with open(path, newline='', encoding='utf-8') as f:

                reader = csv.reader(f)

                added = 0

                for row in reader:

                    if not row:

                        continue

                    name = str(row[0]).strip()

                    if name and name not in self.names:

                        self.names.append(name)

                        self.names_listbox.insert(tk.END, name)

                        added += 1

            self.set_status(f"Imported {added} names from {os.path.basename(path)}")

        except Exception as e:

            messagebox.showerror("Error", f"Failed to import CSV: {e}")


    def remove_selected(self):

        sel = self.names_listbox.curselection()

        if not sel:

            return

        idx = sel[0]

        name = self.names_listbox.get(idx)

        self.names_listbox.delete(idx)

        self.names.remove(name)

        self.set_status(f"Removed {name}")


    def clear_all(self):

        if messagebox.askyesno("Confirm", "Clear all names and constraints?"):

            self.names_listbox.delete(0, tk.END)

            self.names.clear()

            self.fixed.clear()

            self.avoid_pairs.clear()

            self.avoids_listbox.delete(0, tk.END)

            self.set_status("Cleared all data")


    # ---- Fixed & avoids ----

    def set_fixed(self):

        name = self.fixed_name_var.get().strip()

        if not name:

            messagebox.showinfo("Info", "Enter a name")

            return

        if name not in self.names:

            messagebox.showerror("Error", f"{name} not in names list")

            return

        seat = int(self.fixed_seat_var.get())

        total = self.rows_var.get() * self.cols_var.get()

        if seat < 0 or seat >= total:

            messagebox.showerror("Error", f"Seat index must be between 0 and {total-1}")

            return

        # ensure no other name fixed to same seat

        for s, n in list(self.fixed.items()):

            if s == seat and n != name:

                messagebox.showerror("Error", f"Seat {seat} already fixed to {n}")

                return

        # remove prior fixed if exists for that name

        for s, n in list(self.fixed.items()):

            if n == name and s != seat:

                del self.fixed[s]

        self.fixed[seat] = name

        self.set_status(f"Fixed {name} -> seat {seat}")


    def clear_fixed(self):

        self.fixed.clear()

        self.set_status("Cleared fixed seats")


    def add_avoid(self):

        a = self.avoid_a_var.get().strip()

        b = self.avoid_b_var.get().strip()

        if not a or not b:

            return

        if a == b:

            messagebox.showinfo("Info", "Choose two different names")

            return

        if a not in self.names or b not in self.names:

            messagebox.showerror("Error", "Both names must be in the names list")

            return

        pair = frozenset((a, b))

        if pair in self.avoid_pairs:

            messagebox.showinfo("Info", "Pair already exists")

            return

        self.avoid_pairs.add(pair)

        self.avoids_listbox.insert(tk.END, f"{a}  -  {b}")

        self.set_status(f"Added avoid: {a} vs {b}")


    def clear_avoids(self):

        self.avoid_pairs.clear()

        self.avoids_listbox.delete(0, tk.END)

        self.set_status("Cleared avoid pairs")


    # ---- Generation logic ----

    def generate_arrangement(self):

        total_seats = self.rows_var.get() * self.cols_var.get()

        names = list(self.names)

        n_names = len(names)

        if n_names == 0:

            messagebox.showerror("Error", "No names to seat")

            return

        if n_names > total_seats:

            messagebox.showerror("Error", f"Not enough seats: {total_seats} seats for {n_names} names")

            return


        # Starting arrangement with empty seats

        seats = [None] * total_seats

        # place fixed

        for seat_idx, name in self.fixed.items():

            if seat_idx < 0 or seat_idx >= total_seats:

                messagebox.showerror("Error", f"Fixed seat {seat_idx} out of range")

                return

            seats[seat_idx] = name

            if name in names:

                names.remove(name)


        max_tries = max(1, self.max_tries_var.get())

        found = False

        arrangement = None

        for attempt in range(max_tries):

            random.shuffle(names)

            temp = seats.copy()

            i = 0

            # fill empty seats

            for idx in range(total_seats):

                if temp[idx] is None and i < len(names):

                    temp[idx] = names[i]

                    i += 1

            # check avoids

            if self.check_avoids(temp):

                arrangement = temp

                found = True

                break

        if not found:

            messagebox.showwarning("Warning", f"Could not find arrangement after {max_tries} tries")

            self.set_status("No valid arrangement found")

            return

        self.current_arrangement = arrangement

        self.display_arrangement(arrangement)

        self.set_status(f"Arrangement generated (attempt {attempt+1})")


    def check_avoids(self, arrangement):

        # compute adjacency map

        rows = self.rows_var.get()

        cols = self.cols_var.get()

        total = rows * cols

        for r in range(rows):

            for c in range(cols):

                idx = r * cols + c

                name = arrangement[idx]

                if not name:

                    continue

                # neighbors

                neighbors = []

                for dr, dc in ((-1,0),(1,0),(0,-1),(0,1)):

                    rr, cc = r+dr, c+dc

                    if 0 <= rr < rows and 0 <= cc < cols:

                        nidx = rr*cols + cc

                        neighbor = arrangement[nidx]

                        if neighbor:

                            neighbors.append(neighbor)

                for nb in neighbors:

                    if frozenset((name, nb)) in self.avoid_pairs:

                        return False

        return True


    # ---- Display ----

    def display_arrangement(self, arr):

        # textual

        self.output_text.delete(1.0, tk.END)

        for i, name in enumerate(arr):

            self.output_text.insert(tk.END, f"{i}: {name or ''}\n")

        # redraw grid

        self.redraw_grid()


    def redraw_grid(self, event=None):

        self.grid_canvas.delete("all")

        rows = self.rows_var.get()

        cols = self.cols_var.get()

        if rows <=0 or cols <=0:

            return

        w = self.grid_canvas.winfo_width()

        h = self.grid_canvas.winfo_height()

        cell_w = w / cols

        cell_h = h / rows

        arr = self.current_arrangement if self.current_arrangement else [None]*(rows*cols)

        for r in range(rows):

            for c in range(cols):

                idx = r*cols + c

                x0 = c*cell_w

                y0 = r*cell_h

                x1 = x0 + cell_w

                y1 = y0 + cell_h

                self.grid_canvas.create_rectangle(x0, y0, x1, y1, width=1)

                name = arr[idx] if idx < len(arr) else None

                display = name if name else ""

                self.grid_canvas.create_text((x0+x1)/2, (y0+y1)/2, text=display, anchor=tk.CENTER)

                # mark fixed seats with small dot

                if idx in self.fixed:

                    self.grid_canvas.create_oval(x1-12, y0+4, x1-4, y0+12, fill="black")


    # ---- Save/Load ----

    def save_csv(self):

        if not self.current_arrangement:

            messagebox.showinfo("Info", "No arrangement to save")

            return

        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")])

        if not path:

            return

        try:

            with open(path, 'w', newline='', encoding='utf-8') as f:

                writer = csv.writer(f)

                writer.writerow(["seat_index","name"])

                for i, name in enumerate(self.current_arrangement):

                    writer.writerow([i, name or ""])

            self.set_status(f"Saved arrangement to {os.path.basename(path)}")

            messagebox.showinfo("Saved", f"Arrangement saved to {path}")

        except Exception as e:

            messagebox.showerror("Error", f"Failed to save CSV: {e}")


    # ---- Utilities ----

    def set_status(self, msg):

        self.status_var.set(msg)



if __name__ == '__main__':

    app = SeatingGeneratorApp()

    app.mainloop()

https://github.com/gagandeep44489/DiscreteStrucutreAndAlgoApp/blob/main/Seating%20Arrangement%20Generator.py

Comments

Popular posts from this blog

NAND / NOR Logic Simulator: A Python Desktop App for Understanding Universal Logic Gates

Subset Sum Problem Visualizer Using Python (Dynamic Programming GUI Tool)

String Matching Algorithm Trainer (KMP & Rabin-Karp) – Python Desktop App