Modifying variable outside of function

when save is pressed the triggered event handler checks all the fields and if ok saves the input (please tell the user that the input was saved)
so where do you see the need for this strange test variable ?
(by the way you don’t have to disable the save button because the event handler is running all the time, unless you dare threading to optimize) :upside_down_face:

1 Like

"""
	test project
	erst benötigte Module laden
"""
import tkinter as tk		# für die GUI
from tkinter import ttk		# bessere widgets für die GUI
import sqlite3 as sql		# für die Datenbank
# from string import digits	# alle Zahlen von 0 bis 9
import re


"""
	Datenbank erzeugen falls nicht besteht
	Datenbank wird im aktuellen Projektverzeichnis erstell
	Verbindung zur Datenbank erstellen (con), später mit try:  Fehler abfangen
	cursor zeigt auf den  aktuellen Datensatz
	SQL Befehl erstellen
"""
db_name = "tutorial.db"
con = sql.connect(db_name)
cursor = con.cursor()

sql_query = ("""CREATE TABLE IF NOT EXISTS users(
	id integer PRIMARY KEY AUTOINCREMENT,
	name VARCHAR(40) NOT NULL,
	password VARCHAR(30) NOT NULL,
	email VARCHAR(40) NOT NULL,
	age integer,
	gender VARCHAR(1),
	address VARCHAR(60)
	);""")

"""
	SQL Befehl ausführen
	commit schließt den Befehl  ab
"""
cursor.execute(sql_query)
con.commit()

"""
	sqlite_master ist die zentrale Tabelle der Datenbank in der alle Information
	über angelegte Datenbanken, tabellen, Felder hinterlegt sind

cursor.execute("SELECT * FROM sqlite_master WHERE type  = 'table';")
print(cursor.fetchone())
"""

""" variables """
min_name =  3
max_name =  40
min_pw = 6
max_pw = 30
min_email = 4
max_email = 40
age_len = 0
min_age = 10
max_age = 99
min_adr = 0
max_adr = 40
arial_20 =("Arial",18)
no_save = 0
print(no_save)

""" Classes """
"""	Functions """

def insert_users(some_data):
	try:
		con = sql.connect(db_name)
		cursor = con.cursor()

		sql_query = """INSERT INTO users(name, password, email, age, gender, address)
		 VALUES(?,?,?,?,?,?)"""
		cursor.execute(sql_query, some_data)
		con.commit()
		con.close()
	except sql.Error as e:
		lbl_errmsg.config(text=f"An error occurred: {e}")


def on_name_change(*args):
	global no_save
	l = len(name_var.get())
	if name_var.get() == "":
		lbl_name_info.config(text=f"min {min_name} chars", foreground="black")
		ent_name.focus_set()
		print(no_save)
		no_save += 1
	elif l in range(min_name,max_name):
		lbl_name_info.config(text="OK", foreground="green")
		return
	elif l >= max_name:
		lbl_name_info.config(text="to long",foreground="red")
		ent_name.focus_set()
		no_save +=1
		return
	elif l <= min_name:
		lbl_name_info.config(text="to short")
		ent_name.focus_set()
		no_save += 1

def on_pw_change(*args):
	global no_save
	l = len(pw_var.get())
	if pw_var.get() == "":
		lbl_pw_info.config(text=f"min {min_pw} chars", foreground="black")
		ent_pw.focus_set()
		no_save += 1
		return
	elif l in range(min_pw,max_pw):
		lbl_pw_info.config(text="OK", foreground="green")
		return
	elif l >= max_pw:
		lbl_pw_info.config(text="to long",foreground="red")
		ent_pw.focus_set()
		no_save += 1
		return
	elif l <= min_pw:
		lbl_pw_info.config(text="to short")
		ent_pw.focus_set()
		no_save += 1

def valid_email(email_input):
	return bool(re.match( r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',email_input))

def on_email_change(*args):
	global no_save
	l = len(email_var.get())
	x = (email_var.get())
	if x == "":
		lbl_email_info.config(text=f"min {min_email} chars", foreground="black")
		ent_email.focus_set()
		no_save += 1
		return
	elif l in range(min_email,max_email):
		if valid_email(x):
			lbl_email_info.config(text="valid", foreground="green")
			return
		else:
			lbl_email_info.config(text="invalid", foreground="green")
			ent_email.focus_set()
			no_save += 1
			return
	elif l >= max_email:
		lbl_email_info.config(text="to long",foreground="red")
		ent_email.focus_set()
		no_save += 1
		return
	elif l <= min_email:
		lbl_email_info.config(text="to short")
		ent_email.focus_set()
		no_save += 1

def on_age_change(*args):
	global no_save
	if age_var.get() =="":
		lbl_age_info.config(text="OK", foreground="green")
		return
	try:
		n = int(age_var.get())
	except ValueError:
		lbl_age_info.config(text="only numbers", foreground="black")
		ent_age.focus_set()
		no_save += 1
		return
	if n in range(min_age, max_age):
			lbl_age_info.config(text="OK", foreground="green")
			return
	elif n >= max_age:
		lbl_age_info.config(text="too old", foreground="red")
		ent_age.focus_set()
		no_save += 1
		return
	elif n <= min_age:
		lbl_age_info.config(text="too young", foreground="red")
		ent_age.focus_set()
		no_save += 1

def on_gender_change(age):
	global no_save
	list = ["m","w","d",""]
	if (age in list):
		pass
	else:
		cbx_gender.focus_set()
		no_save += 1

def on_adr_change(*args):
	global no_save
	l = len(adr_var.get())
	if l == 0:
		lbl_adr_info.config(text="empty", foreground="green")
		return
	elif l in range(min_adr,max_adr):
		lbl_adr_info.config(text="ok", foreground="green")
		return
	elif l > max_adr:
		lbl_adr_info.config(text="too long",foreground="red")
		ent_adr.focus_set()
		no_save +=1
		return
	elif l < min_adr:
		lbl_adr_info.config(text="too long",foreground="red")
		ent_adr.focus_set()
		no_save += 1

""" validates the complete form """
def check_form():
	global no_save
	no_save = 0	# reset error count
	on_name_change()
	on_pw_change()
	on_email_change()
	on_age_change()
	on_gender_change(cbx_gender.get())
	on_adr_change()
	if no_save == 0:
		""" save to database"""
		my_data = (ent_name.get(),ent_pw.get(),ent_email.get(),ent_age.get(),cbx_gender.get(),ent_adr.get())
		insert_users(my_data)
		"""clear error message"""
		lbl_errmsg.config(text="")
		"""clear form"""
		ent_name.delete(0,tk.END)
		ent_pw.delete(0,tk.END)
		ent_email.delete(0,tk.END)
		ent_age.delete(0,tk.END)
		cbx_gender.delete(0,tk.END)
		ent_adr.delete(0,tk.END)
		ent_name.focus_set()
	else:
		lbl_errmsg.config(text="Bitte Formular prüfen", foreground="red")

"""
	create main window, set position
"""

root = tk.Tk()
root.title('User im System')
root.geometry("800x400+50+50")
root.minsize(width=900, height=400)
root.maxsize(width=1000, height=600)

""" create Widgets """

""" Name """
lbl_name = ttk.Label(root, text="Name:* ", width=10, font=arial_20, padding=5)
lbl_name.grid(row=0, column =0, padx='5', pady='5')

name_var = tk.StringVar()
name_var.trace("w", on_name_change)

ent_name = tk.Entry(root, width=max_name, font=arial_20,textvariable=name_var)
#name_var.pack()
ent_name.grid(row=0, column =1, padx='5', pady='5', sticky="w")

lbl_name_info = ttk.Label(root, text="Name: ")
lbl_name_info.grid(row=0, column =2, padx='5', pady='5')

# Clear the name field.
name_var.set("")

""" password """
lbl_pw = ttk.Label(root, text="Passwort:* ", width=10, font=arial_20, padding=5)
lbl_pw.grid(row=1, column =0, padx='5', pady='5')

pw_var = tk.StringVar()
pw_var.trace("w", on_pw_change)

ent_pw = tk.Entry(root, width=max_pw, font=arial_20,textvariable=pw_var)
ent_pw.grid(row=1, column =1, padx='5', pady='5',sticky="w")

lbl_pw_info = ttk.Label(root, text="")
lbl_pw_info.grid(row=1, column =2, padx='5', pady='5')

# Clear the pw field.
pw_var.set("")

""" email """
lbl_email = ttk.Label(root, text="E-Mail:* ", width=10, font=arial_20, padding=5)
lbl_email.grid(row=2, column =0, padx='5', pady='5')

email_var = tk.StringVar()
email_var.trace("w", on_email_change)

ent_email = tk.Entry(root, width=max_email, font=arial_20,textvariable=email_var)
ent_email.grid(row=2, column =1, padx='5', pady='5',sticky="w")

lbl_email_info = ttk.Label(root, text="")
lbl_email_info.grid(row=2, column =2, padx='5', pady='5')

# Clear the pw field.
email_var.set("")

""" age """
lbl_age = ttk.Label(root, text=f"Alter ({min_age},{max_age}): ", width=10, font=arial_20, padding=5)
lbl_age.grid(row=3, column =0, padx='5', pady='5')

age_var = tk.StringVar()
age_var.trace("w", on_age_change)

ent_age = tk.Entry(root, width=age_len, font=arial_20,textvariable=age_var)
ent_age.grid(row=3, column =1, padx='5', pady='5',sticky="w")

lbl_age_info = ttk.Label(root, text="")
lbl_age_info.grid(row=3, column =2, padx='5', pady='5')

# Clear the pw field.
age_var.set("")


""" Gender """
lbl_gender = ttk.Label(root, text="Geschlecht: (m/w/d)", font=arial_20, padding=5)
lbl_gender.grid(row=4, column=0, padx='5', pady='5')
#Adding combobox drop down list

cbx_gender = ttk.Combobox(root,values=["m","w","d"],width=1,font=arial_20)
cbx_gender.configure(font=arial_20)

cbx_gender.grid(row=4, column=1, padx='5', pady='5', sticky='w')

""" adr """
lbl_adr = ttk.Label(root, text="Adresse ", width=10, font=arial_20, padding=5)
lbl_adr.grid(row=5, column =0, padx='5', pady='5')

adr_var = tk.StringVar()
adr_var.trace("w", on_adr_change)

ent_adr = tk.Entry(root, width=max_adr, font=arial_20,textvariable=adr_var)
ent_adr.grid(row=5, column =1, padx='5', pady='5',sticky="w")

lbl_adr_info = ttk.Label(root, text= "Adresse: ")
lbl_adr_info.grid(row=5, column =2, padx='5', pady='5')

# Clear the pw field.
adr_var.set("")

lbl_errmsg = ttk.Label(root,text="")
lbl_errmsg.grid(row=6, column=1)

lbl_info = ttk.Label(root, text="Felder mit * sind Pflichtfelder")
lbl_info.grid(row=7, column=1)


# Commando etc hinzufügen
btn_name = ttk.Button(text="save", command=check_form)

btn_name.grid(row=8, column=1)
# Felder leeren
ent_name.focus_set()

#	Endlosschleife, wartet auf Events aus der GUI

root.mainloop()

# für module
#if __name__ == "__main__":
#	main()

Perhaps now you understand.
I am shure thats not the best code and not the best solution, but as I wrote I am new in GUI programming and especially in Python GUI programming. Till now I just wrote some small scripts in Python, and so I am only a bit more than a beginner in Python.

Regards
Rainer

You’re fine. Take no notice of people who try to guilt you into avoiding a perfectly good language feature. Global variables exist for a reason.

1 Like

This is approximately what I expected, and indeed it would be relatively easy to replace the global variable for this code I think.

A minimal change would be to replace

global no_save
no_save = 0	# reset error count
on_name_change()
on_pw_change()
on_email_change()
on_age_change()
on_gender_change(cbx_gender.get())
on_adr_change()

with

no_save = (
    on_name_change()
    + on_pw_change()
    + on_email_change()
    + on_age_change()
    + on_gender_change(cbx_gender.get())
    + on_adr_change()
)

and replace all instances of

on_save += 1
return

with

return True

whilst replacing

return

with

return False

(I’m here abusing that True == 1 and False == 0 to make the drop-in replacement look a little nicer to my eyes.)

(I’m not missing some reason why these functions shouldn’t return values, am I?)

(I’m using + instead of or because or short-circuits, which would cause the program to run differently if you rely on any side effects.)

Edit: Note you would also need to change the places where a function exits without an explicit return

There’s other things you could improve. And there’s various ways to polish the code after the change I suggested. But the form seems to work, which is the most important thing. And it is one of the nice things about Python that one can botch things together relatively easily. I wouldn’t want to be a reason for you to have less fun with Python.

One other small thing I noticed: You have “to small” and “to long” instead of “too small” and “too long” in one place.

whilst I understand what you’re saying, that my elitism/perfectionism could make this space less welcoming for beginners (which would be bad), I would also feel bad if I told someone how to use global without warning them not to, especially with this use-case. Because I genuinely think that discovering the pattern

def f():
  global a
  a += 1
  return

def g():
  global a
  a += 1
  return

global a
a = 0
f()
g()

before you discover the pattern

def f():
  return 1

def g():
  return 1

a = f() + g()

would be a net negative.

But this is a completely different approach. And you will often find that, in practice, it gets you nothing - you end up having boilerplate everywhere to stash the value back into its storage location, and there’s still effectively a single global variable, only now you have failed at reentrance.

I will say it again: There is nothing inherently wrong with global variables.

I’m coming a different way: a GUI is a talk between your program and the GUI loop and none of those two dictates the other
a nice counter example is the debugger mentioned by @petercordia
I think the beneficial aspect of this post is to draw a borderline between the mechanical and intentional use of a programming feature (wider than Python) :innocent:

1 Like

well, I’m a wx’er and as an OO’er I’m still fascinated that a simple app can be written in such a way
@petercordia has kindly bend to a somewhat improvement, but there is still plenty of room for doing more … :smiling_face_with_three_hearts:

1 Like

This sounds personal? (to you)

Because it has very little to do with (the code in) this thread.
In the code provided @muekno, the global variable only ever increments immediately before function returns.
I was not suggesting a code pattern wherein a pseudo-global variable gets stashed. There is no need. Because @muekno wrote a nice clean code architecture.

Which means that your proposal wasn’t actually helping the OP.

well, as the user advances in using Pything as a programming language he may stumble over a code analyser
if you run pylint on the code you get quite a few smells and using global always throws a warning which definitely does not help in future editings, I think :sweat_smile:

From the docs you linked to:

How to use pylint
Pylint isn't smarter than you: it may warn you about things that you have conscientiously done or check for some things that you don't care about. During adoption, especially in a legacy project where pylint was never enforced, it's best to start with the --errors-only flag

In other words, the authors of Pylint KNOW that it throws a bunch of warnings about perfectly good code. You have to tune it to what you actually care about.

the smell comes out of the gap between code & perfectly good code
if you want to clean legacy stuff there must be the possibility of an incremental approach (just like they tried with typing in Python)
there are more warnings for name clashes with outer scope and so on
what the warning here points to is an artificially introduced variable whereas traditional coding simply used a return value and more up-to-date try-and-raise
the try does not use an extra variable, reflects very much what is actually done and with this pseudo event character is very near the workings of a GUI (no loop involved though), what can there more be for learning … :sunglasses:

1 Like