class Persona:
def __init__(self,
cognome,
nome,
nascita):
self.cognome = cognome
self.nome = nome
self.nascita = nascita
a_persona = Persona.get()
and through a dedicated form window made with some Entry widgets, I want that infos that users write on it will be saved in a .csv file.
In a first (well working) attempt, I used to write a " .get() " method for each element in the class (cognome.get(), and so onâŚ) without creating any class for âPersonaâ.
Now, Iâm trying this way just because I guess that modifying single attributes if needed would be simple with a class (and a little more well written code for readability), but IDE returns this:
NameError: name 'a_persona' is not defined
So, I tried a different indentation and another error raises:
NameError: cannot access free variable 'Persona' where it is not associated with a value in enclosing scope
Is there a particular method for record data with classes using tkinter?
as the error states, âPersonaâ class has no attributes â.get()â. If you want to get the information for the class attributes, you need to get familiar with âgettersâ and âsettersâ. This is the proper way of obtaining class attribute information outside the class.
Otherwise, you can also try the following if you want an in-class variable with the class attribute information:
class Persona:
a_persona = tuple()
def __init__(self,
cognome,
nome,
nascita):
self.cognome = cognome
self.nome = nome
self.nascita = nascita
self.a_persona = self.cognome, self.nome, self.nascita
print(self.a_persona) # print results for test verification purposes
test = Persona('john', 'larry', 'rick') # Instantiate class
print(test.a_persona[1])
print(test.a_persona[2])
print(test.a_persona[3])
Here, I have just created a tuple of the required information.
Although not directly related, I am posting it here because the video touches on the subject of âgettersâ and âsettersâ of which you should familiarize yourself with. As I stated above, this is one of the preferred ways of getting attribute information from objects (class instances).
There is also a way of using decorators by which you can get object attribute information without having to use the class methods with parentheses.
Once you go to that location (open location âChapter05â), download files chapter5_01, chapter5_02, chapter5_03. Make sure that you have saved all three files to the same folder. Open module chapter5_03 and run it. As youâll see, it has similar properties to your desired application.
First Name
Last Name
Entry widgets
Saving info to .csv file
This is not for copy and pasting for your application, however. Use it as a reference upon which you can build your own unique application. If youâre new to Python and Tkinter, I would recommend reading this book to follow along. Like I stated before, it is not trivial and there are a lot of subtle concepts and nuances that you have to understand to fully appreciate the example.
One last point. Above, you wanted to execute the following line of code:
a_persona = Persona.get() # I am assuming you inadvertently had the wrong indentation
# and that you had instantiated it somewhere else not shown
Since âPersonaâ is a class you defined, there is no auto âget()â method associated with it or generated for it. All methods have to be explicitly defined by you. In your code there was no
def get(self):
# other code here if need be
return yourVariableHere
This is why you were getting an error (there was no get() method defined in your class).
I overlooked one mistake in the code above and shown explicitly here for reference.
a_Persona = Persona.get() # Here, you would not use the name of the class 'Persona' as the name of the object created
So, assuming you instantiated the object like this in the IDLE prompt (interpreter/shell)
(in actual code you would use the class attributes by way of the entry widgets):
user1 = Persona('Domenico', 'Morretti', '08/23/1998')
print(user1.get()) # You may now get access to the 'get()' method via the object created
The point that I am making here is that you have to get the class methods with the object name and not the name of the class itself as proposed in the original code.
Indeed: there is the property builtin class that is most commonly used as a decorator, and is in fact the greatly preferred, Pythonic way to control getting and setting data on classes instead of Java-style getters and setters, because they allow users of your class to access (and set, if you choose) properties just like normal attributes, and you to make attributes computed/specially handled, or read-only, without users having to change any of their code.
Of course, you only need this if your attribute has to be computed or handled in some way (e.g., computing an attribute nome_e_cognome from the given and surname, or vice versa). If youâre just setting the given name, surname and birth date without doing anything to them, you donât need a getter at allâjust access the attribute directly, self.nome, etc inside the class, or persona_instance.nome outside it, as @onePythonUser demonstrates.
However, thereâs an even better way to handle this: if the Persona class exists primary to store data about a person, as opposed to take specific actions, then youâre likely much better off using a Python dataclass instead, as it simplifies your code while doing a bunch of useful things for you and your users. So instead of needing all of, e.g.:
class Persona:
def __init__(self,
cognome,
nome,
nascita):
self.cognome = cognome
self.nome = nome
self.nascita = nascita
def __eq__(self, other):
return (self.cognome == other.cognome
and self.nome == other.nome
and self.nascita == other.nascita)
def __repr__(self):
return (f"Persona(cognome={self.cognome!r}, "
f"nome={self.nome!r}, "
f"nascita={self.nascita!r})")
to have a basic Persona class that you can construct, compare for equality and print, you can instead just do
and everything above and more will be automatically generated for you.
Furthermore, since your stated goal here is to allow converting your class to CSV, a dataclass makes that easy without having to list all your attributes manually (which is easy to make mistakes on or forget to include something), since you can get them as a Python dictionary or a tuple using dataclasses.asdict() and dataclasses.astuple() that you can easily loop over or feed to the Python standard library csv module. Iâd recommend the latter as the more correct solution, but you could probably get away with something as simple as:
@dataclasses.dataclass
class Persona:
...
def as_csv(self):
return ",".join(str(a) for a in dataclasses.astuple(self))
I created this code with a labelframe widget where the user can enter the required information.
I am not familiar with manipulating files (I am just delving into the subject tbh) so hopefully you can
get help with this subject. Currently, the way that I have it set up, the information entered rewrites
the previously entered information.
import tkinter as tk
import csv
class App(tk.Tk):
def __init__(self):
super().__init__()
# Create main window
self.title("Terminale Utente")
tk.Tk.geometry(self, '260x160')
# Create labelFrame and title
group_1 = tk.LabelFrame(self, padx = 15, pady = 10,
text = "Informazione Personale")
group_1.pack(padx = 10, pady = 5)
# Create entry labels
tk.Label(group_1, text = "Nome").grid(row = 0)
tk.Label(group_1, text = "Cognome").grid(row = 1)
tk.Label(group_1, text = "Nascita").grid(row = 2)
# Create entries and class attributes
self.nome = tk.Entry(group_1)
self.nome.grid(row = 0, column = 1, sticky = tk.W)
self.cognome = tk.Entry(group_1)
self.cognome.grid(row = 1, column = 1, sticky = tk.W)
self.nascita = tk.Entry(group_1)
self.nascita.grid(row = 2, column = 1, sticky = tk.W)
# Create button to save information to csv file
self.btn_submit = tk.Button(self, text = "Salva Informazioni", command = self.saveToCSV)
self.btn_submit.pack(padx = 10, pady = 10)
def saveToCSV(self):
infoFields = ['Nome', 'Cognome', 'Nascita']
userInfo = [self.nome.get(), self.cognome.get(), self.nascita.get()]
print(self.nome.get(), self.cognome.get(), self.nascita.get()) # Print for test purposes only
with open('user_information.csv', 'w') as csvfile:
csvwriter = csv.writer(csvfile)
# writing the fields
csvwriter.writerow(infoFields)
# writing the data rows
csvwriter.writerow(userInfo)
if __name__ == "__main__":
app = App()
app.mainloop()
Not a bad start overall (though I canât really comment much on the Tkinter aspects, as Iâm a Qt user and have never toyed around with Tk myself). I do have a few comments.
Most prominently, youâll notice that thereâs a lot of duplication listing the field names, no fewer than six different placesâthis is a so-called âcode smellâ, and usually a sign that you should refactor your code. This makes it harder to read and to add a new field, and a lot easier to make a mistake and introduce a bug while doing so.
To avoid this, I suggest storing the field names in a list as a module or class-level constant, say FIELD_NAMES, and then iterating that as needed to create your entry boxes and extract the data from them. So, assuming we have
self.fields = {}
for row, field_name in enumerate(FIELD_NAMES):
tk.Label(group_1, text=field_name).grid(row=row)
entry = tk.Entry(group_1)
entry.grid(row=row, column=1, sticky=tk.W)
self.fields[field_name] = entry
Now, saving to CSV becomes simpler too, and instead of
def saveToCSV(self):
infoFields = ['Nome', 'Cognome', 'Nascita']
userInfo = [self.nome.get(), self.cognome.get(), self.nascita.get()]
print(self.nome.get(), self.cognome.get(), self.nascita.get()) # Print for test purposes only
with open('user_information.csv', 'w') as csvfile:
csvwriter = csv.writer(csvfile)
# writing the fields
csvwriter.writerow(infoFields)
# writing the data rows
csvwriter.writerow(userInfo)
we can do
def saveToCSV(self):
user_info = [entry.get() for entry in self.fields.values()]
print(*user_info) # Print for test purposes only
with open('user_information.csv', 'w') as csvfile:
csvwriter = csv.writer(csvfile)
csvwriter.writerow(FIELD_NAMES)
csvwriter.writerow(user_info)
def saveToCSV(self):
user_info = {field: entry.get() for field, entry in self.fields.items()}
print(user_info) # Print for test purposes only
with open('user_information.csv', 'w') as csvfile:
csvwriter = csv.DictWriter(csvfile, FIELD_NAMES)
csvwriter.writeheader()
csvwriter.writerow(user_info)
Now, our code is much more concise, and adding a new field is as simple as adding its name to the FIELD_NAMES list.
Your file handling generally looks fine, with the exception that you should always specify the encoding (almost always UTF-8 unless you have a good reason not to) when reading and writing text files, i.e. open(..., encoding="UTF-8"). Otherwise, it will fall back to the system default encoding for legacy reasons, which is almost never what you want over explicitly specifying an encoding.
Its actually not too difficult to adapt what you have to add a new row to the CSV each time the user clicks the Save button. From an IO perspective, the only change needed is changing the w mode to a to append to the file if it already exists instead of overwriting it, plus moving the step of writing the headers outside saveToCSV (to e.g. a separate helper function called by __init__) so it only gets called once, and either having that helper open the file as w to overwrite the version from the last run, or check that it exists and skip writing the headers if it already does, to combine data from multiple runs.
thank you for the insight. Yes, I implemented this way, and it is more concise. By the way, in order for the code to work, we have to include a âself.â in front of the list name wherever it is being used.
As in the two lines of code:
for _row, field_name in enumerate(self.FIELD_NAMES):
csvwriter.writerow(self.FIELD_NAMES)
I will add code such that the subject headers are written once during the init method so that only subsequent additions of names are appended thereafter.
with the help from @CAM-Gerlach, we came up with the following solution. You will have to define
your own file directory for the variable csvFilePath, however.
import tkinter as tk
import csv
import os.path
class App(tk.Tk):
FIELD_NAMES = ['Nome', 'Cognome', 'Nascita']
csvFilePath = '/folder1/folder2/folder3/folder_n/user_information.csv'
def __init__(self):
super().__init__()
# Create main window
self.title("Terminale Utente")
tk.Tk.geometry(self, '260x160')
# Create labelframe and title
group_1 = tk.LabelFrame(self, padx = 15, pady = 10,
text = "Informazione Personale")
group_1.pack(padx = 10, pady = 5)
self.fields = {}
for _row, field_name in enumerate(self.FIELD_NAMES):
tk.Label(group_1, text=field_name).grid(row = _row)
entry = tk.Entry(group_1)
entry.grid(row =_row, column = 1, sticky = tk.W)
self.fields[field_name] = entry
# Create button
self.btn_submit = tk.Button(self, text = "Salva Informazioni", command = self.saveToCSV)
self.btn_submit.bind("<Return>", self.saveToCSV) # Add carriage Return functionality to button
self.btn_submit.pack(padx = 10, pady = 10)
self.fields[self.FIELD_NAMES[0]].focus_set() # Put prompt inside widget entry by default
self.createFile() # Create file if one not already created
def createFile(self):
# If file does not exist, create file and write subject headers
if not os.path.exists(self.csvFilePath):
with open('user_information.csv', 'w', encoding = 'utf-8') as csvfile:
csvwriter = csv.writer(csvfile)
csvwriter.writerow(self.FIELD_NAMES)
csvfile.close()
def saveToCSV(self, *args):
user_info = [entry.get() for entry in self.fields.values()]
# Write user info to file
with open('user_information.csv', 'a', encoding = 'utf-8') as csvfile:
csvwriter = csv.writer(csvfile)
csvwriter.writerow(user_info)
# Clear entries
for index in enumerate(self.fields):
self.fields[self.FIELD_NAMES[index[0]]].delete(0, 'end')
self.fields[self.FIELD_NAMES[0]].focus_set() # Reset prompt focus to first entry
if __name__ == "__main__":
app = App()
app.mainloop()
From here, you should have enough to add additional bells and whistles to your liking, i.e., button to remove outdated users, more entry cells, etc. For example, from the Cookbook files from the previous post, you can force the user to enter their ânascitaâ according to a format by way of the re module for regular expressions.
To note, thatâs only necessary if you make it a class attribute rather than a module-level global constant, as I suggested in the sentence before the one quoted (and my code assumed). But thatâs okay too, it just adds slightly more complexity.
No need; instead of hardcoding an arbitrary absolute path, you can simply output it to the current working directory, just like the original code did, store that (default) path as a constant, and actually use it when saving the fileâcurrently, the code doesnât actually work anyway, since as self.csvFilePath is checked but not actually used to save the file.
As mentioned, this should just be the relative path to the file, and should be actually used when writing the file. Additionally, as a constant it should be named in UPPER_CASE, not camelCase (which, BTW, shouldnât be used in standard Python style; snake_case should be used instead as it is more idiomatic and easier to read). Finally, I suggest making this a pathlib.Path, as it makes the existence check and other things simpler and more elegant. So replace import os.path with the recommended from pathlib import Path up top, and then this should be just
CSV_FILE_PATH = Path('user_information.csv')
With the change above, instead of using the legacy os.path for this check, you can just use the .exists() method of Path objects. Additionally, since youâre in a function, its much cleaner and more idiomatic to check if the path does exists and return immediately, rather than indent everything else under this check. So youâd have:
if self.CSV_FILE_PATH.exists():
return
with open(...):
...
Here you should use the actual path youâve specified above rather than hardcoding another, so this should be:
with open(self.CSV_FILE_PATH, 'w', encoding='UTF-8') as csvfile:
No need to manually close the file as thatâs what using open() as a context manager (i.e. inside a with block) does for you, so this line can simply be removed
You take arbitrary variable-length positional arguments here (*args) but donât actually need or use them, so that should simply be removed (also, donât forget snake_case not camelCase for names, unless you have a very strong reason otherwise).
Yup, its a great idea to clear the fields after savingâI was going to suggest that myself, but didnât want to overcomplicate my post further. However, you can greatly simplify what youâre doing hereâinstead of looping over the fields dict, getting the current index, indexing into that (appears to be a bug), using that to index into field names, using the field names to index into the fields, and finally performing an action on each one, simply loop over the field entries directly (exactly like I do above when creating them):
for entry in self.fields.values():
entry.delete(0, 'end')
Good idea putting this into a helper function, though Iâd suggest a bit of refactoring to avoid the mostly duplicated three lines of code opening the CSV, creating the CSV writer and then write a row of headers/data. Iâd make this function just do those three things (passing in the data to write), pulling the file existence check outside the function, and then you can use it both when creating the file and when saving data to it.
So, this function should look something like:
def write_csv_row(self, row_data):
with open(self.CSV_FILE_PATH, 'a', encoding='UTF-8') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(row_data)
Then, you use it in __init__() like this:
if not self.CSV_FILE_PATH.exists():
self.write_csv_row(self.FIELD_NAMES)
As you know, in IDLE, when you run a .py module, the IDLE working directory will become the directory wherever that .py module resides. This just so happens to be where I wanted my module to reside. So, by chance, my code was âworkingâ (creating the module in the âcorrectâ folder), however incorrectly - as I have heard it said: The ends donât justify the means. I made the changes (corrections) to the code as per your suggestion. Now the code is correct and the directory explicit.
By the way, the reason why I provided the user the option to enter their preferred directory versus the current working directory was just in case if the user had a particular folder where they wanted the .csv file to reside. Maybe if they wanted that list to reside in the same location as other user data where it could be easily accessed (all user information files in the same directory, for example).
Regarding the (*args) addition, there was another test code that I was working on. As soon as I added the carriage Return functionality, it would not work. It wasnât until I added the â*argsâ argument that it did. So, this was a precaution from an earlier code that I was working on.
After carefully reviewing your suggestion regarding rewriting the code for the method calls, it makes sense since we shouldnât have to have two methods with essentially the same code. Thus, you only check if there is an existing .csv file when running the application at startup, and not every time when adding user info to the list.
Overall, your recommendations have been implemented. Yes, this is much better code.
Again, thank you and I appreciate your insights!.
import tkinter as tk
import csv
from pathlib import Path
class App(tk.Tk):
FIELD_NAMES = ['Nome', 'Cognome', 'Nascita']
CSV_FILE_PATH = Path('/folder_tree/user_information.csv')
def __init__(self):
super().__init__()
# Create main window
self.title("Terminale Utente")
tk.Tk.geometry(self, '260x160')
# Create labelframe and title
group_1 = tk.LabelFrame(self, padx = 15, pady = 10,
text = "Informazione Personale")
group_1.pack(padx = 10, pady = 5)
self.fields = {}
for _row, field_name in enumerate(self.FIELD_NAMES):
tk.Label(group_1, text=field_name).grid(row = _row)
entry = tk.Entry(group_1)
entry.grid(row =_row, column = 1, sticky = tk.W)
self.fields[field_name] = entry
# Create button
self.btn_submit = tk.Button(self, text = "Salva Informazioni", command = self.update_list)
self.btn_submit.bind("<Return>", self.update_list) # Add carriage Return functionality to button
self.btn_submit.pack(padx = 10, pady = 10)
self.fields[self.FIELD_NAMES[0]].focus_set() # Put prompt inside widget entry by default
if not self.CSV_FILE_PATH.exists():
self.write_csv_row(self.FIELD_NAMES)
def update_list(self, *args):
user_info = [entry.get() for entry in self.fields.values()]
self.write_csv_row(user_info)
def write_csv_row(self, row_data):
with open(self.CSV_FILE_PATH, 'a', encoding='UTF-8') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(row_data)
# Clear entries
for entry in self.fields.values():
entry.delete(0, 'end')
self.fields[self.FIELD_NAMES[0]].focus_set() # Reset prompt focus
if __name__ == "__main__":
app = App()
app.mainloop()
Looks pretty good now, thanks. One last followup commentâright now, you have the following code in write_csv_row to clear the current field entries and reset the prompt focus:
However, per the principle of separation of concerns, those should really âbelongâ to the update_list method, since they involve actions in the GUI that should happen after submitting the data to be written, rather than anything to do with writing the CSV itself. And indeed, they arenât necessary when creating the file during program startup, but are executed anyway.
Therefore, you should move these lines to update_list instead. Other than that, looks pretty good!
I follow you logic here. I have updated the code at my end. Weâll keep the write_csv_row method as concise as possible such that it only writes to the file and any other statements in a separate method.
This little piece of code sure has been learning experience.