Q:

Tkinter - bug that shows forgotten labels on corner of screen?

Screenshot: http://puu.sh/wiuP7/957bca09ea.png
My code is attached and roughly does the following:
Grabs data from SQL server
For each product it lists all printed products and all scanned products. There could be multiple prints (see P039 on the bottom). It does this using labels.
Every 5 seconds it runs using after(), and when it does it uses grid_forget() to remove all of the "old" labels.. it runs the for loop and the updated labels are there, 5 seconds later with no blinking or any noticeable interruption.
The issue: After an undetermined amount of time it will no longer loop correctly. It will not show any labels and there will be what looks like a pile of labels sitting on the top left corner of my window. However in the screenshot, the pile in the corner is actually because of an instance of the script that I had closed, so the open instance (with the labels all there correctly) is good, so far. The bug in the corner is from a previously closed instance. Nonetheless, if I haven't had opened it again, you would see nothing instead of the list of products and numbers.
This always occurs, no matter what. I have learned that increasing the after() delay also increases the time it takes for it to happen. For example, it happened in roughly 20-30 mins when the delay was 2000 ms, but it took hours (not sure how long, at least 5) when the delay was 10000 ms.
Manually running the for loop at the top of the function using a different function and a button does not fix it.
I am using tkinter on python 3.6.1. I have seen it on windows 10 and Windows Server 2008 r2 through remote desktop
The code of interest is below, this does all the work with updating and such. My entire code is long, but here are my includes as well:
EDIT: It appears that if the program is minimized it will not occur.
import time
import threading
import logging
import datetime
import tkinter as tk
from tkinter import *
import tkinter.scrolledtext as ScrolledText
from tkinter.ttk import Separator, Style
from collections import Counter
import pypyodbc

And here is the updating function:
def build_productionrpt(self, LotDate, Shift):
    for label in self.grid_slaves():
        if int(label.grid_info()["column"]) >= 0 and int(label.grid_info()["row"]) >= 3:
            label.grid_forget()
    self.products = []
    self.r = 3
    self.c = 0

    # Get all printed labels for current date and shift
    labels = ProductionReport.LabelsByShift(LotDate,Shift,option="Labels")
    # Only look at the DatePrinted and the ProductID
    product_batch_sets = [(record[0], record[1]) for record in labels]
    # Sort so that we only get unique values, otherwise there would be a duplicate for every unit.
    product_batch_unique_sets = list(set(product_batch_sets))

    # Get all units for each batch and sort by PrintedDate, UnitNumber, and RFIDSerial. Currently we are not using RFIDSerial for anything in this report
    batch_unit_sets = [(record[0], record[6], record[5]) for record in labels]
    # Sort so that we only get unique values, otherwise there would be a duplicate for every unit.
    batch_unit_unique_sets = list(set(batch_unit_sets))

    # Get all scanned labels for current date and shift, sort by PrintedDate, UnitNumber, and RFIDSerial. Currently we are not using RFIDSerial for anything in this report
    units_scanned_sets = [(record[0], record[6], record[5]) for record in ProductionReport.LabelsByShift(LotDate,Shift,option="Scanned")]
    # Remove all duplicate scans 
    units_scanned_unique_sets = list(set(units_scanned_sets))

    # Begin by going through each row
    for record in product_batch_unique_sets:
        # Get each unique product from the list, if it's already in self.products then it's been Labeled and we can move on
        if record[1] not in self.products:
            # Add to self.products so we don't label it twice
            self.products.append(record[1])
            # Label it on the GUI, using row 3 as the first row.
            # First, make sure the labels haven't gotten too far. If it has, reset the row count and change the column
            if self.r > 35:
                self.c = 1
                self.r = 2
            elif self.r > 35 and self.c == 1:
                self.c = 2
                self.r = 2
            Label(self, text=record[1], font=("Helvetica", 16, "bold")).grid(row=self.r, column=self.c, sticky="nw", rowspan=3)
            # Add 3 rows to r, to make room for the Product label.
            self.r+=3
            # Set the current product to what we just labeled, so we know what to use as a reference when getting batches.
            current_product = record[1]
            # Get all batches for this product and put them in a list
            all_batches = [i for i,v in product_batch_unique_sets if v == current_product]
            # Now take each batch from the list we just made..
            for batch in all_batches:
                # Label it
                # Label(self, text=str(batch), font=("Helvetica", 10)).grid(row=self.r, column=0, sticky="nw")
                # Add 2 rows to make room for the batch and for the unit numbers
                # Get all unitnumbers printed for this specific batch
                all_unitnumbers = [v for i,v,u in batch_unit_unique_sets if i == batch]
                # Sort them so it's readable
                all_unitnumbers.sort()
                # Get all units that were scanned from this batch
                all_scannedunits = [v for i,v,u in units_scanned_unique_sets if i == batch]
                # Sort them so it's readable
                all_scannedunits.sort()
                # Label the scanned units, they are green
                Label(self, text=str(all_scannedunits), font=("Helvetica", 8), wraplength=500, justify="left", fg="green").grid(row=self.r, column=self.c, sticky="nw", padx=20)
                # Add 2 rows to make room for the unscanned units
                self.r+=2
                # This takes all printed units, and subtracts all scanned units from that. What's left is the units that were printed but not scanned
                a = Counter(all_unitnumbers)
                b = Counter(all_scannedunits)
                c = a - b
                # Label all units printed but not scanned
                Label(self, text=str(list(c.elements())), font=("Helvetica", 8), wraplength=500, justify="left", fg="red").grid(row=self.r, column=self.c, sticky="nw", padx=20)
                # Add two rows to make room for the next Product
                self.r+=2

                # all_noscans = [v for i,v,u in units_noscan_unique_sets if i == batch]
                # Label(self, text=str(all_noscans), font=("Helvetica", 8)).grid(row=self.r, column=0, sticky="nw", padx=20)
                # self.r+=3
    if should_continue_prod_report:
        self.after(5000, lambda:self.build_productionrpt(self.lotdate_e.get(), self.shift_e.get()))

A:

I was able to fix this issue by:
Storing all labels in a dictionary with the key being something like ProductVariable + "_label" and the value would be the label
Storing all textvariables in the same dictionary. I had to change the label(root, text=..) to label(root, textvariable=..)
In every for loop, check and see if the label existed. If it does, use set() to update it.
Basically, it was bad practice to create labels every 5 seconds and forget the old ones as a method to updating data live.

