2025, Dec 20 13:00

Stop AttributeError in Python CustomTkinter GUIs: call DAO instance methods correctly and fix SQLite init order

Learn why calling Store.fetch_vehicles on the class passes a GUI as self and triggers AttributeError, plus the right CustomTkinter and SQLite init order.

When a Python GUI calls into a data access layer, it’s easy to forget that instance methods expect an instance. A common slip is to pass a GUI object where a database object is expected, which ends with an AttributeError that looks puzzling at first glance. In this guide we’ll unpack why that happens, show the exact failure pattern, and fix it cleanly. Along the way, we’ll also address an initialization order pitfall that silently breaks SQLite usage.

The setup: calling a DAO method the wrong way

Below is a minimal data access object for SQLite3 and a CustomTkinter frame that tries to pull records. The structure is sound, but there are two subtle problems baked in.

import sqlite3


class Store:

    VEHICLES_DDL = """
                    CREATE TABLE IF NOT EXISTS vehicles (
                        vehicle_key integer NOT NULL PRIMARY KEY AUTOINCREMENT,
                        vehicle_description TEXT NOT NULL
                    )
                """

    SQL_VEHICLE_INSERT = "INSERT INTO vehicles (vehicle_description) VALUES (?)"

    SQL_VEHICLE_SELECT_ALL = "SELECT * FROM vehicles"

    MILEAGE_DDL = """
                  CREATE TABLE IF NOT EXISTS milage (
                    milage_date DATE NOT NULL PRIMARY KEY,
                    vehicle_id INTEGER NOT NULL,
                    milage integer NOT NULL
                  )
                """

    SQL_MILEAGE_INSERT = "INSERT INTO milage ( milage_date , vehicle_id , milage ) VALUES (? , ? , ? )"

    def __init__(self, dbname = "vehicles.sqlite3"):
        # dbname is set too late here
        self.setup_table(self.MILEAGE_DDL)
        self.setup_table(self.VEHICLES_DDL)
        self.dbname = dbname

    def setup_table(self, sql):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(sql)

    def insert_vehicle(self, description):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_INSERT, (description,))

    def insert_milage(self, milage_date, vehicle_id, milage):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_MILEAGE_INSERT, (milage_date, vehicle_id, milage))

    def fetch_vehicles(self):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_SELECT_ALL)
            return cur.fetchall()
import customtkinter as ctk
from store import Store


class MileageInputView(ctk.CTkFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        self.section = ctk.CTkFrame(self)
        self.section.pack(fill="x", padx=5, pady=25)

        # Wrong: passing a GUI instance where a Store instance is expected
        records = Store.fetch_vehicles(self)

The failure presents as an AttributeError that points at the GUI object missing a database attribute:

AttributeError: 'MilageEntryFrame' object has no attribute 'dbname'

What’s really going on

In Python, self is just a conventional name for the first parameter of an instance method. The interpreter automatically passes the instance when you call the method on an instance. When you call a method on the class instead, you must provide that first argument explicitly. That explicit argument must be an instance of the class whose method you are calling, otherwise attribute lookups inside the method reference the wrong object.

>>> class Sample:
...     def task(self):
...         pass
...
>>> type(Sample.task)
<class 'function'>
>>> type(Sample().task)
<class 'method'>

In the GUI snippet, Store.fetch_vehicles(self) tells Python to pass the MileageInputView instance as the first argument to fetch_vehicles. Inside fetch_vehicles, the code tries to use self.dbname, but now self is a GUI frame, not a data access object, so dbname does not exist there. Hence the AttributeError.

There’s a second, independent issue in the data access object: dbname is assigned after methods that depend on it are already called in the constructor. The setup_table method opens sqlite3.connect(self.dbname), but self.dbname is only set later. Even if the first problem is fixed, this initialization order will break database operations early.

The fix: instantiate the DAO and initialize it correctly

First, call the instance method on an actual instance of the data access class. Second, ensure the database path is stored on the instance before any method that needs it is invoked. The corrected code below applies both changes without altering behavior.

import sqlite3


class Store:

    VEHICLES_DDL = """
                    CREATE TABLE IF NOT EXISTS vehicles (
                        vehicle_key integer NOT NULL PRIMARY KEY AUTOINCREMENT,
                        vehicle_description TEXT NOT NULL
                    )
                """

    SQL_VEHICLE_INSERT = "INSERT INTO vehicles (vehicle_description) VALUES (?)"

    SQL_VEHICLE_SELECT_ALL = "SELECT * FROM vehicles"

    MILEAGE_DDL = """
                  CREATE TABLE IF NOT EXISTS milage (
                    milage_date DATE NOT NULL PRIMARY KEY,
                    vehicle_id INTEGER NOT NULL,
                    milage integer NOT NULL
                  )
                """

    SQL_MILEAGE_INSERT = "INSERT INTO milage ( milage_date , vehicle_id , milage ) VALUES (? , ? , ? )"

    def __init__(self, dbname = "vehicles.sqlite3"):
        # Set first, use later
        self.dbname = dbname
        self.setup_table(self.MILEAGE_DDL)
        self.setup_table(self.VEHICLES_DDL)

    def setup_table(self, sql):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(sql)

    def insert_vehicle(self, description):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_INSERT, (description,))

    def insert_milage(self, milage_date, vehicle_id, milage):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_MILEAGE_INSERT, (milage_date, vehicle_id, milage))

    def fetch_vehicles(self):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_SELECT_ALL)
            return cur.fetchall()
import customtkinter as ctk
from store import Store


class MileageInputView(ctk.CTkFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        self.section = ctk.CTkFrame(self)
        self.section.pack(fill="x", padx=5, pady=25)

        # Correct: create an instance, then call the method
        records = Store().fetch_vehicles()

Why this matters for maintainable Python

Understanding how bound methods work is essential when stitching together UI layers and data access layers. It prevents subtle bugs where the wrong object winds up inside a method, and it avoids misleading errors that point at missing attributes on unrelated classes. Equally important is respecting initialization order: attributes must exist before dependent methods use them, particularly when those methods talk to external systems like a SQLite database.

Takeaways

Always call instance methods on actual instances rather than on the class and never substitute a foreign object for self. Set critical attributes, such as the database path, before making any calls that depend on them. With these two principles in place, your CustomTkinter widgets and SQLite-backed DAO will play nicely together, and errors like an unexpected AttributeError on dbname simply won’t occur.