Tkinter: Only allow one Toplevel window instance
<p>I have a tkinter program with multiple windows. Here is the full code in case it's entirety is needed.</p>

<pre><code>import tkinter as tk
import tkinter.scrolledtext as tkst
from tkinter import ttk
import logging
import time


def popupmsg(msg):
    popup = tk.Toplevel()
    popup.wm_title("!")
    label = ttk.Label(popup, text=msg)
    label.pack(side="top", fill="x", pady=10)
    b1 = ttk.Button(popup, text="Okay", command=popup.destroy)
    b1.pack()
    popup.mainloop()


def test1():
    root.logger.error("Test")


def toggle(self):
    t_btn = self.t_btn
    if t_btn.config('text')[-1] == 'Start':
        t_btn.config(text='Stop')

        def startloop():
            if root.flag:
                now = time.strftime("%c")
                root.logger.error(now)
                root.after(30000, startloop)
            else:
                root.flag = True
                return
        startloop()
    else:
        t_btn.config(text='Start')
        root.logger.error("Loop stopped")
        root.flag = False


class TextHandler(logging.Handler):

    def __init__(self, text):
        # run the regular Handler __init__
        logging.Handler.__init__(self)
        # Store a reference to the Text it will log to
        self.text = text

    def emit(self, record):
        msg = self.format(record)

        def append():
            self.text.configure(state='normal')
            self.text.insert(tk.END, msg + '\n')
            self.text.configure(state='disabled')
            # Autoscroll to the bottom
            self.text.yview(tk.END)

        # This is necessary because we can't modify the Text from other threads
        self.text.after(0, append)

    def create(self):
        # Create textLogger
        topframe = tk.Frame(root)
        topframe.pack(side=tk.TOP)

        st = tkst.ScrolledText(topframe, bg="#00A09E", fg="white", state='disabled')
        st.configure(font='TkFixedFont')

        st.pack()

        self.text_handler = TextHandler(st)

        # Add the handler to logger
        root.logger = logging.getLogger()
        root.logger.addHandler(self.text_handler)

    def stop(self):
        root.flag = False

    def start(self):
        if root.flag:
            root.logger.error("error")
            root.after(1000, self.start)
        else:
            root.logger.error("Loop stopped")
            root.flag = True
            return

    def loop(self):
        self.start()


class HomePage(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.menubar = tk.Menu(container)

        # Create taskbar/menu
        file = tk.Menu(self.menubar)
        file.add_command(label="Run", command=lambda: test1())
        file.add_command(label="Stop", command=lambda: test1())
        file.add_separator()
        file.add_command(label="Settings", command=lambda: Settings())
        file.add_separator()
        file.add_command(label="Quit", command=quit)
        self.menubar.add_cascade(label="File", menu=file)

        self.master.config(menu=self.menubar)

        #logger and main loop
        th = TextHandler("none")
        th.create()
        root.flag = True
        root.logger.error("Welcome to ShiptScraper!")

        bottomframe = tk.Frame(self)
        bottomframe.pack(side=tk.BOTTOM)

        topframe = tk.Frame(self)
        topframe.pack(side=tk.TOP)

        self.t_btn = tk.Button(text="Start", highlightbackground="#56B426", command=lambda: toggle(self))
        self.t_btn.pack(pady=5)

        self.exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
        self.exitButton.pack()
        root.setting = False


class Settings(tk.Toplevel):

    def __init__(self, master=None):
        tk.Toplevel.__init__(self, master)
        self.wm_title("Settings")
        print(Settings.state(self))

        exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
        exitButton.pack()


class Help(tk.Toplevel):

    def __init__(self, parent):
        tk.Toplevel.__init__(self, parent)
        self.wm_title("Help")

        exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
        exitButton.pack()


if __name__ == "__main__":
    root = tk.Tk()
    root.configure(background="#56B426")
    root.wm_title("ShiptScraper")
    app = HomePage(root)
    app.mainloop()
</code></pre>

<p>Basically my problem is that clicking the command <code>Settings</code> from the menu brings up a new <code>Settings</code> window each time it's clicked. I can't figure out how make it so it can detect if one window instance is already open or not. I've tried using <code>state()</code> as a check in a method in the <code>HomePage</code> class like</p>

<pre><code>#in it's respective place as shown above
file.add_command(label="Settings", command=lambda: self.open(Settings))

#outside the init as a method
def open(self, window):
    if window.state(self) != 'normal':
        window()
</code></pre>

<p>This returns this error</p>

<pre><code>Exception in Tkinter callback
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/tkinter/__init__.py", line 1550, in __call__
    return self.func(*args)
  File "/Users/user/pythonProjects/ShiptScraper/ShiptScraperGUI.py", line 112, in &lt;lambda&gt;
    file.add_command(label="Settings", command=lambda: self.open(Settings))
  File "/Users/user/pythonProjects/ShiptScraper/ShiptScraperGUI.py", line 139, in open
    if window.state(self) != 'normal':
  File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/tkinter/__init__.py", line 1826, in wm_state
    return self.tk.call('wm', 'state', self._w, newstate)
_tkinter.TclError: window ".4319455216" isn't a top-level window
</code></pre>

<p>I've tried using the <code>winfo_exists()</code> method, but it seems like unless I've already destroyed the window (which I haven't if it hasn't been opened yet) this won't do me any good. Nonetheless here's one of those combo's I've tried</p>

<pre><code>def open(self, window):
    if window.winfo_exists(self) != 1:
        window()
</code></pre>

<p>This of course does nothing. I'm not going to go through every other wrong combination. I've tried because honestly at this point, I can't remember them all. </p>

<p>I have also tried defining these <code>open</code> methods as functions outside of any class and they don't work there either, usually because of confusion of <code>self</code> keywords not being defined outside a class, but needing to be a parameter of the <code>winfo_exists()</code> and <code>state()</code> methods. </p>

<p>I'm also thinking that my problem, in using these functions as methods in the HomePage class, is because whenever I'm referencing <code>self</code>, it's checking <code>HomePage</code>, not whatever window I'm passing as an argument in the method. I'm not really sure though, which is why I'm here.</p>

<p>Really, what I'm trying to do is just create a standard method in my <code>HomePage</code> window that controls how the menu (and possibly buttons later) opens a window. This would logically (in my own psuedocode) be:</p>

<pre><code>def open(window)
   if window does not exist:
        open an instance of window
</code></pre>

<p>Is this possible, Or is there a better approach to window management I should be taking?</p>

<p><strong>Edit:</strong>
I originally neglected to mention that my OS is Mac OSX running Mavericks. Apparently this may be an OSX issue. Also, if you're going to downvote this question at least comment and tell me why/how I can revise it to make it better.</p>

<p>I've now tried these combinations</p>

<pre><code>class Settings(tk.Toplevel):

    def __init__(self, master=None):
        tk.Toplevel.__init__(self, master)
        self.wm_title("Settings")
        # added grab_set()
        self.grab_set()
        #
        print(Settings.state(self))

        exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
        exitButton.pack()
</code></pre>

<p>and</p>

<pre><code>class Settings(tk.Toplevel):

    def __init__(self, master=None):
        tk.Toplevel.__init__(self, master)
        self.wm_title("Settings")
        # added grab_set()
        self.grab_set()
        self.focus()
        #
        print(Settings.state(self))

        exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
        exitButton.pack()
</code></pre>

<p>and</p>

<pre><code>class Settings(tk.Toplevel):

    def __init__(self, master=None):
        tk.Toplevel.__init__(self, master)
        self.wm_title("Settings")
        # added grab_set()
        self.attributes("-topmost", True)
        #
        print(Settings.state(self))

        exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
        exitButton.pack()
</code></pre>

<p>and</p>

<p>class Settings(tk.Toplevel):</p>

<pre><code>def __init__(self, master=None):
    tk.Toplevel.__init__(self, master)
    self.wm_title("Settings")
    # added grab_set()
    self.after(1, lambda: self.focus_force())

    #
    print(Settings.state(self))

    exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=self.destroy)
    exitButton.pack()
</code></pre>

<p><strong>Edit #2:</strong></p>

<p>I've come up with a workaround... I hate it. But it works, at least for now. I'm definitely still hoping for a better solution.</p>

<pre><code>import tkinter as tk
import tkinter.scrolledtext as tkst
from tkinter import ttk
import logging
import time



def popupmsg(msg):
    popup = tk.Toplevel()
    popup.wm_title("!")
    label = ttk.Label(popup, text=msg)
    label.pack(side="top", fill="x", pady=10)
    b1 = ttk.Button(popup, text="Okay", command=popup.destroy)
    b1.pack()
    popup.mainloop()


def test1():
    root.logger.error("Test")


def toggle(self):
    t_btn = self.t_btn
    if t_btn.config('text')[-1] == 'Start':
        t_btn.config(text='Stop')

        def startloop():
            if root.flag:
                now = time.strftime("%c")
                root.logger.error(now)
                root.after(30000, startloop)
            else:
                root.flag = True
                return
        startloop()
    else:
        t_btn.config(text='Start')
        root.logger.error("Loop stopped")
        root.flag = False


class TextHandler(logging.Handler):

    def __init__(self, text):
        # run the regular Handler __init__
        logging.Handler.__init__(self)
        # Store a reference to the Text it will log to
        self.text = text

    def emit(self, record):
        msg = self.format(record)

        def append():
            self.text.configure(state='normal')
            self.text.insert(tk.END, msg + '\n')
            self.text.configure(state='disabled')
            # Autoscroll to the bottom
            self.text.yview(tk.END)

        # This is necessary because we can't modify the Text from other threads
        self.text.after(0, append)

    def create(self):
        # Create textLogger
        topframe = tk.Frame(root)
        topframe.pack(side=tk.TOP)

        st = tkst.ScrolledText(topframe, bg="#00A09E", fg="white", state='disabled')
        st.configure(font='TkFixedFont')

        st.pack()

        self.text_handler = TextHandler(st)

        # Add the handler to logger
        root.logger = logging.getLogger()
        root.logger.addHandler(self.text_handler)

    def stop(self):
        root.flag = False

    def start(self):
        if root.flag:
            root.logger.error("error")
            root.after(1000, self.start)
        else:
            root.logger.error("Loop stopped")
            root.flag = True
            return

    def loop(self):
        self.start()


class HomePage(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

# NEW added a flag for the Settings window
        root.settings = False
        self.menubar = tk.Menu(container)

        # Create taskbar/menu
        file = tk.Menu(self.menubar)
        file.add_command(label="Run", command=lambda: test1())
        file.add_command(label="Stop", command=lambda: test1())
        file.add_separator()

# NEW now calling a method from Settings instead of Settings itself
        file.add_command(label="Settings", command=lambda: Settings().open())
        file.add_separator()
        file.add_command(label="Quit", command=quit)
        self.menubar.add_cascade(label="File", menu=file)

        self.master.config(menu=self.menubar)

        #logger and main loop
        th = TextHandler("none")
        th.create()
        root.flag = True
        root.logger.error("Welcome to ShiptScraper!")

        bottomframe = tk.Frame(self)
        bottomframe.pack(side=tk.BOTTOM)

        topframe = tk.Frame(self)
        topframe.pack(side=tk.TOP)

        self.t_btn = tk.Button(text="Start", highlightbackground="#56B426", command=lambda: toggle(self))
        self.t_btn.pack(pady=5)

        self.exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
        self.exitButton.pack()
        root.setting = False


class Settings(tk.Toplevel):

        def __init__(self, master=None):
            tk.Toplevel.__init__(self, master)

# NEW 'open' method which is being called. This checks the root.setting flag added in the HomePage class
        def open(self):
            #NEW if root setting is false, continue creation of of Settings window
            if not root.setting:
                self.wm_title("Settings")
                # added grab_set()
                Settings.grab_set(self)

            #NEW edited the exitButton command, see close function below
                exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=lambda: close())
                exitButton.pack()
                root.setting = True
            #NEW if the root.settings flag is TRUE this cancels window creation
            else:
                self.destroy()

            #NEW when close() is called it resets the root.setting flag to false, then destroys the window
            def close():
                root.setting = False
                self.destroy()


class Help(tk.Toplevel):

    def __init__(self, parent):
        tk.Toplevel.__init__(self, parent)
        self.wm_title("Help")

        exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
        exitButton.pack()


if __name__ == "__main__":
    root = tk.Tk()
    root.configure(background="#56B426")
    root.wm_title("ShiptScraper")
    app = HomePage(root)
    app.mainloop()
</code></pre>

<p>This feels like a complete and utter hack, I feel dirty looking at it and even dirtier for creating this abomination... but it works, for now at least</p>

<p><strong>EDIT 3:</strong></p>

<p>Added the protocol for window close in Jacob's answer. Had forgotten to account for that. This is the last version I'll share unless I come up with a better approach.</p>

<pre><code>import tkinter as tk
import tkinter.scrolledtext as tkst
from tkinter import ttk
import logging
import time



def popupmsg(msg):
    popup = tk.Toplevel()
    popup.wm_title("!")
    label = ttk.Label(popup, text=msg)
    label.pack(side="top", fill="x", pady=10)
    b1 = ttk.Button(popup, text="Okay", command=popup.destroy)
    b1.pack()
    popup.mainloop()


def test1():
    root.logger.error("Test")


def toggle(self):
    t_btn = self.t_btn
    if t_btn.config('text')[-1] == 'Start':
        t_btn.config(text='Stop')

        def startloop():
            if root.flag:
                now = time.strftime("%c")
                root.logger.error(now)
                root.after(30000, startloop)
            else:
                root.flag = True
                return
        startloop()
    else:
        t_btn.config(text='Start')
        root.logger.error("Loop stopped")
        root.flag = False


class TextHandler(logging.Handler):

    def __init__(self, text):
        # run the regular Handler __init__
        logging.Handler.__init__(self)
        # Store a reference to the Text it will log to
        self.text = text

    def emit(self, record):
        msg = self.format(record)

        def append():
            self.text.configure(state='normal')
            self.text.insert(tk.END, msg + '\n')
            self.text.configure(state='disabled')
            # Autoscroll to the bottom
            self.text.yview(tk.END)

        # This is necessary because we can't modify the Text from other threads
        self.text.after(0, append)

    def create(self):
        # Create textLogger
        topframe = tk.Frame(root)
        topframe.pack(side=tk.TOP)

        st = tkst.ScrolledText(topframe, bg="#00A09E", fg="white", state='disabled')
        st.configure(font='TkFixedFont')

        st.pack()

        self.text_handler = TextHandler(st)

        # Add the handler to logger
        root.logger = logging.getLogger()
        root.logger.addHandler(self.text_handler)

    def stop(self):
        root.flag = False

    def start(self):
        if root.flag:
            root.logger.error("error")
            root.after(1000, self.start)
        else:
            root.logger.error("Loop stopped")
            root.flag = True
            return

    def loop(self):
        self.start()


class HomePage(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

# NEW added a flag for the Settings window
        root.setting = True
        self.menubar = tk.Menu(container)

        # Create taskbar/menu
        file = tk.Menu(self.menubar)
        file.add_command(label="Run", command=lambda: test1())
        file.add_command(label="Stop", command=lambda: test1())
        file.add_separator()

# NEW now calling a method from Settings instead of Settings itself
        file.add_command(label="Settings", command=lambda: Settings().open())
        file.add_separator()
        file.add_command(label="Quit", command=quit)
        self.menubar.add_cascade(label="File", menu=file)

        self.master.config(menu=self.menubar)

        #logger and main loop
        th = TextHandler("none")
        th.create()
        root.flag = True
        root.logger.error("Welcome to ShiptScraper!")

        bottomframe = tk.Frame(self)
        bottomframe.pack(side=tk.BOTTOM)

        topframe = tk.Frame(self)
        topframe.pack(side=tk.TOP)

        self.t_btn = tk.Button(text="Start", highlightbackground="#56B426", command=lambda: toggle(self))
        self.t_btn.pack(pady=5)

        self.exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
        self.exitButton.pack()


class Settings(tk.Toplevel):

        def __init__(self, master=None):
            tk.Toplevel.__init__(self, master)

# NEW 'open' method which is being called. This checks the root.setting flag added in the HomePage class
        def open(self):
            #NEW when close() is called it resets the root.setting flag to false, then destroys the window
            def close_TopLevel():
                root.setting = True
                self.destroy()

            #NEW if root setting is false, continue creation of of Settings window
            if root.setting:
                self.wm_title("Settings")
                #NEW adjust window close protocol and change root.setting to FALSE
                self.protocol('WM_DELETE_WINDOW', close_TopLevel)
                root.setting = False

            #NEW edited the exitButton command, see close function below
                exitButton = tk.Button(self, text="Exit", highlightbackground="#56B426", command=lambda: close_TopLevel())
                exitButton.pack()

            #NEW if the root.settings flag is TRUE this cancels window creation
            else:
                print('shit')
                self.destroy()



class Help(tk.Toplevel):

    def __init__(self, parent):
        tk.Toplevel.__init__(self, parent)
        self.wm_title("Help")

        exitButton = tk.Button(text="Exit", highlightbackground="#56B426", command=quit)
        exitButton.pack()


if __name__ == "__main__":
    root = tk.Tk()
    root.configure(background="#56B426")
    root.wm_title("ShiptScraper")
    app = HomePage(root)
    app.mainloop()
</code></pre>