2. The Shop class: creating, saving and loading

Lines 8–26

Shop is the core of the program and knows nothing about the window. It holds two pieces of data: a dictionary of products and a list of sales. This part covers its first three methods: the constructor, save, which writes everything to disk, and load, which reads it back.

Code for this part

shop_manager.py, lines 8–26
8class Shop:9    def __init__(self):10        self.products = {}11        self.sales = []12        self.load()13 14    def save(self):15        with open(FILE, 'w', encoding='utf-8') as f:16            json.dump({'products': self.products, 'sales': self.sales}, f, ensure_ascii=False, indent=2)17 18    def load(self):19        if os.path.exists(FILE):20            try:21                with open(FILE, 'r', encoding='utf-8') as f:22                    data = json.load(f)23                self.products = data.get('products', {})24                self.sales = data.get('sales', [])25            except (OSError, json.JSONDecodeError, AttributeError):26                self.products, self.sales = {}, []

Click a line number to jump to its explanation; click “line …” in an explanation to highlight that line in the code.

Line by line

line 8class Shop:
Defines the Shop class, a template that keeps related data and behaviour together. Nothing is created yet; the actual object is built on line 65 with Shop().
line 9def __init__(self):
The constructor. Python runs it automatically every time Shop() is called. self is the object being created, and attributes are attached to it through self.
line 10self.products = {}
An empty products dictionary. Each key is a product name and each value is a small dictionary such as {'buy': 1000, 'sell': 1500, 'qty': 3} (purchase price, sale price, stock).
line 11self.sales = []
An empty list of sales. Each sale is a dictionary like {'name': 'pen', 'qty': 2, 'total': 3000}, appended on line 59.
line 12self.load()
Loads any saved data straight away. The order matters: the empty defaults are created first (line 10 and line 11), so if there is no file the object simply keeps them.
line 13
A blank line between two methods.
line 14def save(self):
save writes all of the object's data to the file. It needs no argument besides self, because it takes everything from the object.
line 15with open(FILE, 'w', encoding='utf-8') as f:
Opens the file in write mode ('w'), which replaces any existing content or creates the file. with guarantees the file is closed at the end, even if an error occurs. encoding='utf-8' keeps non-English product names intact. f is the open file.
line 16json.dump({'products': self.products, 'sales': self.sales}, f, ensure_ascii=False, indent=2)
Builds a dictionary with two keys, products and sales, and writes it to f with json.dump. ensure_ascii=False stores non-ASCII text as-is instead of escapes such as \u0645, and indent=2 indents each level by two spaces so people can read the file too.
line 17
A blank line.
line 18def load(self):
load reads the data from the file and puts it on the object. It is only called from the constructor (line 12).
line 19if os.path.exists(FILE):
First checks that the file exists. On the very first run there is no file, so the whole block is skipped.
line 20try:
Starts a try block: an error in any of the lines below jumps to the except clause (line 25) instead of stopping the program.
line 21with open(FILE, 'r', encoding='utf-8') as f:
Opens the file in read mode ('r') with the same UTF-8 encoding.
line 22data = json.load(f)
json.load reads the file and turns its text into Python objects — here a dictionary — stored in data.
line 23self.products = data.get('products', {})
Takes the value of the products key. If the key is missing, get returns its second argument, {}, instead of raising KeyError.
line 24self.sales = data.get('sales', [])
The same for sales, defaulting to an empty list [].
line 25except (OSError, json.JSONDecodeError, AttributeError):
Catches three kinds of error: OSError when the file cannot be opened (no permission, for example), json.JSONDecodeError when it is not valid JSON, and AttributeError when it is valid JSON but not a dictionary (a list, say), because a list has no get method. The parentheses mean “any of these”.
line 26self.products, self.sales = {}, []
On error, resets both attributes in one multiple assignment, so the program starts empty. The right-hand side is a two-item tuple unpacked into the two names on the left.

Note

If the file is corrupt, the program opens quietly with empty data, and the first save (line 15) overwrites the corrupt file completely. An earlier version did not catch AttributeError on line 25, so a file containing, say, [1, 2] crashed the program at start-up.

How it connects to the rest of the code

See also

Shop
line 65 creates one instance named shop, which every user-interface function uses.
self.products
Read or changed in add line 33, edit line 41, delete line 47, sell line 55 and in the table on line 116.
self.sales
Grows in sell line 59 and is summed in report line 63.
save()
Called after every change: line 34, line 42, line 48 and line 60. Its file errors (OSError) are caught on line 90.
load()
Called once, in the constructor line 12.

Check your understanding

Try to answer each question before opening it.

  1. What happens the very first time the program runs, when there is no data file?

    Show answer

    The check on line 19 is false, so load does nothing and the object keeps the empty {} and [] from line 10 and line 11.

  2. What does with on line 15 give you that a plain f = open(...) would not?

    Show answer

    The file is closed automatically when the block ends, even if json.dump raises an error.

  3. What is the difference between data.get('products', {}) and data['products']?

    Show answer

    If the key is missing, get returns {}, while data['products'] raises KeyError.

  4. A data file contains [1, 2]. What happens when the program starts?

    Show answer

    json.load returns a list, data.get raises AttributeError, line 25 catches it, and line 26 starts with empty data.