MVC with tkinter

A way to organise your IA code, not an exam topic: this page is for students who build their IA product in Python with tkinter (the window toolkit that comes with most Python installations). MVC (Model-View-Controller) is not named in the syllabus, and the IA does not ask for any particular structure. It is one common way to organise a program with a user interface (UI: the screens the user sees and works with). The data, the screens and the code that responds to the user go in separate classes, which makes the code easier to test, explain and change. It puts into practice the benefits of modularisation (splitting a program into separate parts, each with one job; B2.3.4). Writing in Java with JavaFX? The Java version of this page is MVC Architecture.


Overview

A first tkinter program often puts everything in one file: the data, the window and the code that runs when a button is clicked. That works for a small app, but as an application grows, mixing them makes it harder to change or test one part without affecting the others. Separating those responsibilities keeps the code organised.

Model-View-Controller (MVC) is an architecture for programs with a user interface: a plan for which parts a program is split into and how those parts work together. It is often called a design pattern (a tried and tested way of organising code). MVC separates your application into three parts, each with its own job. Understanding MVC will help you:

  • Write IA code that a reader can follow
  • Add features with less risk of breaking existing ones
  • Explain the structure of your IA product: Criterion C (System overview) asks for a system model of the key components and how they interact, and Criterion D (Development) asks you to justify the structure of the product

This page builds one small app, a Student Manager, in four files: models.py, views.py, controller.py and main.py. Then it shows how a larger app with several screens can share one controller.

Check that your Python has tkinter before you start. Run import tkinter in Python: no error means it is there. The python.org installers for Windows and macOS include it, and so does the Thonny editor’s own installer on those systems. Some installations leave it out: Python from Homebrew on a Mac needs the separate python-tk package (brew install python-tk), and on Linux tkinter is usually a separate package (for example python3-tk). If you see “No module named ‘_tkinter’”, install that package, or install Python from python.org.


The Three Components

Model: The Data

The Model holds your application’s data and business logic (the rules the data must follow and the calculations done with it). It knows nothing about the user interface: it does not know that buttons, text boxes or windows exist.

Responsibilities:

  • Store data (attributes, lists)
  • Check the data against its rules (for example, a grade from 0 to 100)
  • Perform calculations and operations
  • Read and write the data in files (see File I/O in Apps)

Example: A Student class with a name, a grade and validation (checks that refuse data that breaks the rules). It goes in models.py.

class Student():
    def __init__(self, name, grade):
        if name == None or name.strip() == "":
            raise ValueError("Name cannot be empty")
        if grade < 0 or grade > 100:
            raise ValueError("Grade must be 0-100")
        self._name = name      # one underscore: private by agreement
        self._grade = grade

    def get_name(self):
        return self._name

    def get_grade(self):
        return self._grade

    def set_grade(self, grade):
        if grade < 0 or grade > 100:
            raise ValueError("Grade must be 0-100")
        self._grade = grade

    def __str__(self):    # __str__ is Python's toString()
        return self._name + " (" + str(self._grade) + ")"

raise ValueError(...) stops the constructor or the method and passes an exception (an object that describes what went wrong) back to the code that called it. The Controller below catches it with try/except and shows the message to the user. The syllabus names try, except and finally but not raise, so treat raising your own exceptions as a tool for your IA.

A Collection Model

Many IA projects also need a class that manages a collection of model objects: adding, removing, searching and sorting. It goes in models.py too.

class StudentList():
    def __init__(self):
        self._students = []

    def add_student(self, s):
        self._students.append(s)

    def remove_by_name(self, name):
        for i in range(len(self._students)):
            if self._students[i].get_name() == name:
                self._students.pop(i)
                return True
        return False

    def find_by_name(self, name):
        for i in range(len(self._students)):
            if self._students[i].get_name() == name:
                return self._students[i]
        return None

    def get_all(self):
        return self._students

    def size(self):
        return len(self._students)

In this design, models.py does not import tkinter. If a model class creates a window, a button or a message box, something is in the wrong place. A Model that needs no window can be tested on its own: create a StudentList, add students, search for one and check the results, all without opening a window.

One underscore or two? The classes on this page mark their attributes as private with one underscore (self._name), the convention used across this site’s Python. Python does not stop other code from using _name: the underscore is an agreement between programmers, and other classes should use the get methods. Some starter projects use two underscores at the start (self.__name). Inside the Student class, Python renames that attribute to _Student__name, a feature called name mangling, so that a subclass or other code is less likely to use it by accident. It does not restrict access the way Java’s private does: code that uses the new name can still reach it. Either way, other classes should use the get methods. (Names with two underscores at both ends, such as __init__ and __str__, are Python’s special methods and are not renamed.)

View: The Display

The View is what the user sees and interacts with. In tkinter, this is the window with its widgets (the controls: buttons, text boxes, labels and lists) and the layout that arranges them. Keep the View as simple as possible: it displays data and passes user actions on to the Controller, but it does not decide what to do with them. Rules about the data belong in the Model.

Responsibilities:

  • Create and arrange the widgets
  • Display data provided by the Controller
  • Report user actions (button clicks, text input) to the Controller

Controller: The Coordinator

The Controller is the coordinator between Model and View. It handles user actions, updates the Model, and tells the View what to display.

Responsibilities:

  • Respond to user actions from the View
  • Call Model methods to add, remove or change data
  • Update the View so that it shows the Model’s current data
  • Catch errors and show the user a clear message

How They Interact

flowchart LR
    V["View<br>(UI)"] -- "user action" --> C["Controller<br>(coordinator)"]
    C -- "update data" --> M["Model<br>(data and rules)"]
    M -- "return data" --> C
    C -- "update display" --> V
  1. User clicks a button in the View
  2. Controller handles the event: reads the input from the View and checks that it can be used (for example, that the grade is a whole number)
  3. Controller updates the Model: adds a student, changes a grade, and so on. The Model checks its own rules (a grade from 0 to 100)
  4. Controller updates the View: refreshes the list, shows a message

In this design, the Model and View do not talk to each other directly. All communication goes through the Controller.

There are several versions of MVC. In the one on this page the View is passive: it does not read the Model, and the Controller hands it the text to show. MVC Architecture: Versions of MVC describes the others, including the observer pattern from the HL syllabus.


MVC in Practice: Student Manager

The Model classes are shown above. Below are the View, the Controller and the short file that starts the app and connects them.

The View

import tkinter as tk
from tkinter import messagebox


class StudentView():
    def __init__(self, window):
        # Create the widgets, so the Controller can connect to them straight away
        self._name_entry = tk.Entry(window, width=30)
        self._grade_entry = tk.Entry(window, width=30)
        self._add_button = tk.Button(window, text="Add")
        self._remove_button = tk.Button(window, text="Remove")
        self._search_button = tk.Button(window, text="Search")
        self._status = tk.Label(window, text="Ready")
        self._display = tk.Listbox(window, width=40, height=12)

        # Arrange them in a grid of rows and columns
        tk.Label(window, text="Name").grid(row=0, column=0, sticky="w", padx=8, pady=4)
        self._name_entry.grid(row=0, column=1, columnspan=3, sticky="we", padx=8, pady=4)
        tk.Label(window, text="Grade (0-100)").grid(row=1, column=0, sticky="w", padx=8, pady=4)
        self._grade_entry.grid(row=1, column=1, columnspan=3, sticky="we", padx=8, pady=4)
        self._add_button.grid(row=2, column=1, pady=4)
        self._remove_button.grid(row=2, column=2, pady=4)
        self._search_button.grid(row=2, column=3, pady=4)
        self._status.grid(row=3, column=0, columnspan=4, sticky="w", padx=8)
        self._display.grid(row=4, column=0, columnspan=4, sticky="nsew", padx=8, pady=8)

    # The Controller says which of its methods each button runs
    def set_handlers(self, add, remove, search):
        self._add_button.config(command=add)
        self._remove_button.config(command=remove)
        self._search_button.config(command=search)

    # What the user typed
    def get_name(self):
        return self._name_entry.get().strip()

    def get_grade_text(self):
        return self._grade_entry.get().strip()

    # What the Controller asks the View to show
    def clear_inputs(self):
        self._name_entry.delete(0, tk.END)
        self._grade_entry.delete(0, tk.END)

    def show_status(self, text):
        self._status.config(text=text)

    def show_students(self, lines):
        self._display.delete(0, tk.END)
        for i in range(len(lines)):
            self._display.insert(tk.END, lines[i])

    def confirm(self, question):
        return messagebox.askyesno("Confirm", question)

Notice: the View creates the widgets and offers simple methods (get what was typed, show a message, show a list, ask a yes or no question), but it makes no decisions. It does not know what happens when a button is clicked: set_handlers lets the Controller choose. Apart from main.py, which creates the main window and starts mainloop(), every tkinter call in the app is in this file, so the Model and the Controller import nothing from tkinter.

command=add gives the button the method itself, to run on each click. Write the method’s name without brackets: command=self.handle_add() would run the method once, while the window is being built, and give the button its result (None), so clicks would then do nothing.

The Controller

from models import Student


class StudentController():
    def __init__(self, model, view):
        self._model = model
        self._view = view
        view.set_handlers(self.handle_add, self.handle_remove, self.handle_search)
        self.refresh_display()

    def handle_add(self):
        name = self._view.get_name()
        grade_text = self._view.get_grade_text()

        if name == "":
            self._view.show_status("Error: Name cannot be empty.")
            return

        try:
            grade = int(grade_text)
        except ValueError:
            self._view.show_status("Error: Grade must be a whole number.")
            return

        try:
            s = Student(name, grade)
        except ValueError as error:
            self._view.show_status("Error: " + str(error))
            return

        self._model.add_student(s)
        self._view.show_status("Added: " + str(s))
        self._view.clear_inputs()
        self.refresh_display()

    def handle_remove(self):
        name = self._view.get_name()
        if name == "":
            self._view.show_status("Error: Enter a name to remove.")
            return

        if self._view.confirm("Remove " + name + "?") == True:
            if self._model.remove_by_name(name) == True:
                self._view.show_status("Removed: " + name)
                self._view.clear_inputs()
                self.refresh_display()
            else:
                self._view.show_status("Not found: " + name)

    def handle_search(self):
        name = self._view.get_name()
        if name == "":
            self._view.show_status("Error: Enter a name to search.")
            return

        found = self._model.find_by_name(name)
        if found != None:
            self._view.show_status("Found: " + str(found))
        else:
            self._view.show_status("Not found: " + name)

    def refresh_display(self):
        students = self._model.get_all()
        lines = []
        for i in range(len(students)):
            lines.append(str(i + 1) + ". " + str(students[i]))
        self._view.show_students(lines)

The Controller checks the input in two steps. Text that is not a whole number is caught when int(grade_text) raises a ValueError; a whole number outside 0 to 100 is refused by the Student constructor, and the Controller shows that message. Because the Controller only uses the View’s methods, it can also be tested with no window open: give it a stand-in object with the same methods (get_name, show_status and the rest) in place of the View, call handle_add(), and check what it was asked to show.

Starting the App (Connecting the Parts)

main.py creates the window, the Model, the View and the Controller, connects them, and starts tkinter’s event loop (the loop that waits for clicks and key presses and runs the matching handlers).

import tkinter as tk
from models import StudentList
from views import StudentView
from controller import StudentController

window = tk.Tk()
window.title("Student Manager")

model = StudentList()
view = StudentView(window)
controller = StudentController(model, view)

window.mainloop()

This file stays small. The rest of the application’s work happens in the three components. The order matters: the Model and the View are created first, because the Controller uses both as soon as it is created. window.mainloop() comes last: it keeps the window open and responding until the user closes it, so code written after it runs only then.


One Controller for Every Screen

The Student Manager has one window, so main.py can pass the Model and the View straight to the Controller. An IA product often has several screens (a login, a main window, a window for adding a record), and they all need the same data. One way to give them that is a single shared controller that every screen can reach. It is built as a singleton: a class that allows only one object of itself to be made.

from models import StudentList


class AppController():
    _instance = None    # the one shared AppController, once it exists

    @staticmethod
    def get_instance():
        if AppController._instance == None:
            AppController()    # the constructor records itself as the one instance
        return AppController._instance

    def __init__(self):
        if AppController._instance != None:
            raise RuntimeError("Use AppController.get_instance(), not AppController()")
        AppController._instance = self
        self._students = StudentList()    # the data every screen shares
        self._current_user = None

    def get_students(self):
        return self._students

    def get_current_user(self):
        return self._current_user

    def set_current_user(self, name):
        self._current_user = name

Any screen gets the same controller by calling AppController.get_instance():

controller = AppController.get_instance()
controller.set_current_user("Sam")                 # on the login screen
students = AppController.get_instance().get_students()   # on any other screen

How it works:

  • _instance is a class attribute (one value shared by the class, like a static field in Java). It starts as None and then holds the one controller.
  • The first call to get_instance() makes the controller; every later call returns that same object, so every screen works with the same data.
  • Java stops a second object with a private constructor. Python has no private constructors, so the constructor refuses a second object by raising an error, and get_instance() is the way in.
  • @staticmethod marks get_instance() as a method you call on the class itself, with no object and no self. The @ line (a decorator) is outside the exam-style Python subset, so treat it as a tool for your IA.

Use it for data that every screen needs, such as the logged-in user or the list every screen works with. A singleton is shared global state: if everything goes into it, screens become hard to follow. Most data can still travel through the Controller as it does in the Student Manager. Multi-Window Apps with tkinter shows how windows open, pass data and send answers back.

Note for IB CS learners: the singleton is one of the design patterns in the HL syllabus (B3.2.5). In the exam you are expected to explain what it does and when it is useful, not to write it. SL students can use it here as part of their IA’s structure.


Why MVC Matters for Your IA

Explaining Your Structure

MVC is one structure you can choose; the IA does not require it. If you use it, it gives you clear points to make in two criteria:

  • Criterion C (System overview) asks for a system model with “the key components, their relationships, the rules governing their interaction”. A diagram of your Model, View and Controller classes, with arrows for the connections between them, shows those components and relationships.
  • Criterion D (Development) asks you to “justify the structure of the product, why the structure is appropriate”. MVC gives your code a modular design (separate parts, each with one job), and you can explain how that helps your own product, for example:
    • Model classes can be tested without the GUI (graphical user interface: the windows, buttons and text boxes)
    • The look of a screen can change without changing the Model
    • The Controller can be changed without changing the data classes

Building on a starter project or a tutorial? Acknowledge it, keep a copy of the original code, and make clear which parts you changed or added. The logic that solves your problem is your own work.

Adding Features is Easier

With MVC, a new feature often touches the three parts in turn. For example, to show the class’s average grade:

  1. Model: Add the method (for example, average_grade() in StudentList)
  2. View: Add the widget and a method to show it (for example, a label and show_average(text))
  3. Controller: Call the Model’s method and pass the result to the View (for example, at the end of refresh_display())

Debugging is Easier

When something goes wrong:

  • Wrong data stored? Check the Model
  • Window laid out wrongly? Check the View
  • Button does the wrong thing, or the list is out of date? Check the Controller

Common MVC Mistakes

Putting logic in the View. If your View has if statements that decide what to do with the user’s input, move them: rules about the data go in the Model, and decisions about what happens after a click go in the Controller.

Putting window code in the Model. If models.py imports tkinter or shows a message box, the Model is doing the View’s job. A Model should work without any window open, so that you can test it on its own.

Skipping the Controller. Some students give each button a function that changes the Model directly. This works for small apps, but the code becomes hard to change and fix as the app grows. With a Controller between them, the View and the Model do not depend on each other.

Making a second tk.Tk() for a second window. A tkinter app has one main window, made by tk.Tk(); other windows are tk.Toplevel windows that belong to it. Each tk.Tk() creates a separate Tcl interpreter (the engine that runs tkinter’s windows), so widgets and tkinter variables made for one cannot be used with the other. Toplevel windows share the main window’s interpreter. See Multi-Window Apps with tkinter.


The MVC Checklist

If you use the design on this page for your IA, these checks show whether the three parts are still separate.

Rule Check
models.py imports nothing from tkinter  
The View creates and arranges widgets but makes no decisions about the data  
The Controller handles every user action  
The Model can be tested without opening a window  
Each class has one clear responsibility  
main.py only creates the parts, connects them and starts mainloop()  

Quick Check

Q1. In the Student Manager on this page, which class creates the widgets and the dialogs?

Q2. A student connects the Add button to the Controller's handler but writes brackets after the handler's name. What happens when the app runs?

Q3. A program calls AppController.get_instance() for the second time. What does it return?

Q4. Inside a class called Student, what does naming an attribute self.__name, with two underscores at the start, do?

Q5. An app uses the shared AppController from this page. A second window needs the same list of students as the first. What should the second window do?


Practice Exercises

Core

  1. Identify the components (4 marks): A music student’s practice-log app has the four classes below. Identify whether each class belongs to the Model, the View or the Controller.
    • Session stores an instrument, a date and a number of minutes, and refuses a negative number of minutes.
    • PracticeLog keeps a list of Session objects and works out the total number of minutes practised.
    • PracticeWindow creates the entry boxes, a Save button and a list of past sessions, and has methods that show text and a list.
    • PracticeManager gives the Save button a method that reads the entry boxes, adds a new Session to the PracticeLog and asks the window to show the updated list.
  2. Fix the violation (4 marks): A student’s ProductView class runs this code when the Save button is clicked: it reads the price, and if the price is below 0 it shows “Invalid price” and stops. Explain where the check that a price is not negative belongs, where the decision to show the message belongs, and why each belongs there. You are not required to write code.

Extension

  1. Build a calculator (6 marks): Construct a simple calculator using MVC:
    • Model: a Calculator class with add(), subtract(), multiply() and divide() methods that each take two numbers and store the answer in a _result attribute, and a get_result() method. divide() refuses a divisor of 0 by raising a ValueError.
    • View: two entry boxes for the numbers, four operation buttons and a result label, with methods to read the numbers and show a result.
    • Controller: reads the two numbers, calls the matching Model method and shows the result. Text that is not a number, and division by 0, show a message instead.
  2. Add sorting (6 marks): Construct a sorting feature for the Student Manager: a “Sort by Grade” button in the View, a sort_by_grade() method in the StudentList model that uses selection sort to put the students in order from the highest grade to the lowest, and a handler in the Controller that sorts the list and refreshes the display.

Challenge

  1. Club events app (8 marks): Construct an MVC application for a school club that keeps a list of Member objects and a list of Event objects, where each event has its own list of the members attending it. The user can add a member, add an event, sign a member up for an event (once only) and show who is attending an event. Plan your Model classes, the View’s widgets and methods, and the Controller’s methods on paper first, then implement them.

  2. Justify the shared controller (6 marks): A classmate’s IA product has a login window, a main window and an “add booking” window, and every window reaches the bookings through AppController.get_instance(). Explain two benefits of using one shared controller for this product, and one risk of putting too much into it. You are not required to write code.


Connections


© EduCS.me. A resource hub for Computer Science education. EduCS is an independent educational resource. It is not affiliated with, or endorsed by, the International Baccalaureate Organization.