Let's start with what I think will be the easiest bit: the data format of your save-files.
CSV
Csv is quite old and not very common nowadays. I'd say most Python developers use json most of the time, but also yaml, toml, and xml are common. That would convert your "database" from
Element,Symbol,AtomicNumber,AtomicMass,Type,WikiLink
Hydrogen,H,1,1.007,Nonmetal,https://en.wikipedia.org/wiki/Hydrogen
...
to
[
{
"Element": "Hydrogen",
"Symbol": "H",
"Atomic Number": 1,
"Atomic Mass": 1.007,
"Type": "Nonmetal",
"Wiki Link": "https://en.wikipedia.org/wiki/Hydrogen"
},
...
]
which is way more readable, and will be very easy to deal with in Python. For example:
>>> import json
>>> with open("elements.json", "r") as f:
... data = json.load(f)
...
>>> data
[{'Element': 'Hydrogen', 'Symbol': 'H', 'Atomic Number': 1, 'Atomic Mass': 1.007, 'Type': 'Nonmetal', 'Wiki Link': 'https://en.wikipedia.org/wiki/Hydrogen'}, {'Element': 'Carbon'}]
Now, as you can see this is essentially the same data structure in Python as it is in json. So I'm gonna make my own life a little easier and just keep this as a variable in a Python-file for now, but you don't have to do that.
Structure and requirements
I think that your application should be packaged. How you'd typically structure a Python package is like this:
.
├── README.rst
├── periodic_table
│ ├── __init__.py
│ ├── elements.py
│ ├── loader.py
│ └── widgets.py
└── requirements.txt
1 directory, 6 files
One thing is that it displays purpose - you can see that all of these files are part of the package periodic_table. It also creates a namespace. The file __init__.py will also mean that you can refer to the modules in this package by changing from
from widgets import Element, Key, Demo
to
from periodic_table.widgets import Element, Demo, Key
and now the reader immediately knows where these functions come from. Personally I think that an even better suggestion is:
from periodic_table import widgets
# and then use widgets.Element, etc.
But I concede that I don't abide by this strongly.
But that's not packaged!
I cheated a little just now - I showed a directory with just a requirements.txt file. Better than this would be to configure the package. I'll be modern and will skip setup.py, and I will also be very minimal with the metadata. We now have:
.
├── README.rst
├── periodic_table
│ ├── __init__.py
│ ├── elements.py
│ ├── loader.py
│ └── widgets.py
├── pyproject.toml
└── setup.cfg
1 directory, 7 files
pyproject.toml
You can consider this file "boilerplate code" for packages and just copy the content without deeper understanding (for now).
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
setup.cfg
This is where you define all your metadata for the package. It doesn't all matter if you just want to make it installable, but if you wanted to publish something on PyPI you should make some effort.
[metadata]
name = periodic-table
version = 1.0.0
[options]
packages = find:
python_requires = >=3.6
setup_requires =
wheel
install_requires =
tkinter
pandas
[options.entry_points]
console_scripts =
show-periodic-table = periodic_table:main
With all of this done, your package would be installable with pip! You can then from the directory just type pip install . and the package periodic_table would be installed in that environment. And this also allows us to define commands to call on certain functions!
And this sort of segways us into the next point - your current entry-point is confusing.
if __name__ == "__main__": and other tings
When I look at your filenames my interpretation is that main.py is the entry-point, yet at the bottom of widgets.py you have:
if __name__ == '__main__':
root = tk.Tk()
Element(root,'Hydrogen').pack(padx=5,pady=20)
Key(root,e_type='Halogen').pack()
Demo(root,'Hydrogen').pack()
root.mainloop()
Here I can clearly see that widgets.py is "executable" (interpretable?) and that this file is intended to start a tkinter loop. I'm guessing that this just an artifact from when you first learnt to use tkinter, but this is basically what we should have done in your main.py file, so let's do that.
main.py
The point behind having if __name__ == "__main__": is that it means that the code wrapped in that condition only will be called upon if the current file is executed directly, like this:
$ python3 main.py # We want this to run our program
But not be loaded if the module is imported, like this:
from periodic_table import main # We do not want this to run our program
Entry-points
What I did in setup.cfg (go back and have a look at the end of the file) is to also set up an additional way to call on your program. The lines
console_scripts =
show-periodic-table = periodic_table:main
declare that there should be an alias set that you can use from your shell (on Linux and macOS, not sure how this works from PowerShell and others) called show-periodic-table, and that this should call on the function main in periodic_table/__init__.py. You could also call the function show_periodic_table and place it in widgets.py if you wanted to - in that case the line would be show-periodic-table = periodic_table.widgets:show_periodic_table.
To fit your code with this, we only really have to rename main.py into __init__.py, add like two lines, and indent most of the existing lines. I will, however, make one other tiny change just to try and keep the code almost working. We can keep widgets.py exactly as it is today, but remember how I mentioned I'd just store the otherwise-json data in a variable? That allows me to get rid of the loader while also simplifying packaging (I'm not gonna explain what I mean - this review has to end some time today...). So we have:
.
├── README.rst
├── periodic_table
│ ├── __init__.py
│ ├── constants.py
│ └── widgets.py
├── pyproject.toml
└── setup.cfg
1 directory, 6 files
constants.py
ELEMENTS = [
{
"Element": "Hydrogen",
"Symbol": "H",
"Atomic Number": 1,
"Atomic Mass": 1.007,
"Type": "Nonmetal",
"Wiki Link": "https://en.wikipedia.org/wiki/Hydrogen"
},
...
]
SEC_LAYOUT = [['x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x' ,'x'],
...
]
__init__.py
This is now where we have our entry point. I've left it called just main, but an even more fitting name would probably be show_periodic_table.
import tkinter as tk
from tkinter import ttk
from periodic_table.widgets import Element, Key, Demo
from periodic_table.constants import MAIN_LAYOUT, SEC_LAYOUT, ELEMENTS, TYPES
def main():
root = tk.Tk()
root.title('Periodic Table')
# Frame for entire table
table = tk.Frame(root)
table.pack()
...
# Placing the representation in the frame
Demo(repr_tab, elements[0]).grid(row=i,column=j+1,rowspan=5)
root.mainloop()
And I think I'll let that wrap this session up. The code would need some other modifications to work entirely (since we now already have a list of element dictionaries instead of having to load from csv), but I'll leave that for you to figure out.