Modeling Systems

Objects and Persistent Data


Learning Objectives

  • You can convert instances into dictionaries containing values JSON can save.
  • You can reconstruct objects through existing validation and model operations.
  • You can keep file-format code separate from the model and user interface.
  • You can test independent snapshots and restored objects.

JSON can represent the records from Part 5: dictionaries containing strings, numbers, booleans, lists, and other supported values. We will call these dictionaries plain-data records. A LoanItem instance also has methods such as borrow(), and JSON does not automatically know which of its attributes to save.

That is a useful boundary. A save file should describe the information needed to reconstruct valid objects, not expose every internal detail of their implementation.

Convert an Object into a Record

The counter from Defining Classes and Creating Objects gives us a small example. This version also checks that its starting value is a nonnegative integer:

No files opened Select a file to edit

The method returns a new dictionary containing a supported plain value. Changing record["value"] leaves the counter at 3, so the printed JSON is {"value": 3}.

For this flat record, a new dictionary makes entry replacement independent. A nested mutable field would need its own copying decision, as in Part 4.

Change the final expression from json.dumps(counter.to_record()) to json.dumps(counter) and run it again. This raises TypeError: JSON does not automatically convert our instance. Restore the explicit conversion afterward.

A LoanItem needs the catalogue fields from Parts 4 and 5: "id", "name", "category", and "available". The field names describe the saved format; they need not match every attribute name. The next exercise supplies the item class so you can add its export operation.

Export an item

0 / 10 points

Complete LoanItem.to_record() in the supplied class. Return a new dictionary with exactly "id", "name", "category", and "available" fields, using the item’s current values. The "id" field comes from item_id. Do not print, read input, or change the item. Changing the returned dictionary must not change the item, and each call must return an independent record. A borrowed item must export "available": False. Keep the supplied initializer and borrowing methods.

Work in main.py. Open Show file tree, select check.py, and Run to try the supplied sample checks. Add cases for the remaining requirements, including boundary values and repeated operations where relevant. Keep test calls in check.py so importing main.py does not print or ask for input. Submit sends the files for grading against the full contract and returns feedback.

Export the Collection Without Exposing Its List

An EquipmentLibrary can export a list of records by using its items’ export operations. The result should preserve insertion order and include unavailable items too: a save file must remember the whole library.

Callers receive dictionaries describing the library. These records are separate from its live objects, the class instances currently used by the program. Clearing the returned list or changing a returned record must leave the library unchanged.

This differs from find, whose contract deliberately returns an existing item instance. Calling borrow() on that item changes the library’s item. Both public methods are useful because their effects are explicit.

The next exercise supplies the item and library classes. Use these checks while implementing its to_records method: the exported snapshot may change without changing the live library. The supplied check.py imports EquipmentLibrary and includes this snapshot check. Run that file and extend its cases as you work.

library = EquipmentLibrary()
item = library.add_item("E10", "Camera", "media")
records = library.to_records()
records[0]["available"] = False
records.append({})
assert item.available == True
assert len(library.to_records()) == 1

Export a library

0 / 15 points

Complete EquipmentLibrary.to_records() in the supplied classes. Call each item’s to_record() and return a new list of those independent records in insertion order. Do not expose the internal item list or return live items. Empty libraries return []. Do not print or change the library. Changing the returned list or any record must leave the library unchanged. Keep repeated names belonging to different identifiers. Keep the supplied item and library operations.

Work in main.py. Open Show file tree, select check.py, and Run to try the supplied sample checks. Add cases for the remaining requirements, including boundary values and repeated operations where relevant. Keep test calls in check.py so importing main.py does not print or ask for input. Submit sends the files for grading against the full contract and returns feedback.

Reconstruct Through the Model’s Rules

Reading JSON gives us dictionaries and lists. To get instances with methods again, pass the saved values through the operations that create valid objects.

The complete example below checks the record and leaves the numeric rule to the initializer. Predict the final object value and the saved record before running it.

No files opened Select a file to edit

The initializer rejects an invalid numeric value. The conversion function checks the saved shape without repeating that rule.

Try a saved value of -1. Identify whether the conversion function or the initializer raises. Then trace exporting and rebuilding small objects:

Pause Before Reconstructing a Collection

Keep the Counter and counter_from_record definitions in the preceding editor and replace the statements after them with this example. Predict the three output lines before running it:

current = Counter(4)
snapshot = current.to_record()
try:
current = counter_from_record({"value": -1})
except ValueError:
print("Record rejected.")
snapshot["value"] = 9
print(current.value)
print(snapshot["value"])

Before moving on, explain:

  1. Which function or method rejects the negative value?
  2. Does the assignment replace the object named current when the call raises?
  3. Why does changing snapshot leave the live counter unchanged?

The output is Record rejected., 4, and 9. The initializer rejects the value before the conversion function can return. The assignment therefore leaves current referring to its original object. The snapshot is a separate dictionary.

Try a saved value of 6 instead. The call now returns a new counter, so current.value becomes 6; the snapshot can still be changed independently. Continue when you can explain both paths. A complete library will apply the same rules to several records before returning a replacement.

Reconstruct a Complete Library

A library needs a larger document, for example:

data = {
"items": [
{"id": "E10", "name": "Camera", "category": "media", "available": False}
]
}

The document holds the exported item records in an items list. The supplied checked_item_records(data) helper checks the document’s shape, the required fields, and the boolean availability values. It returns the records without changing the input. The exercise handout states the exact rules.

Reconstruction should reuse EquipmentLibrary.add_item to validate and trim text and reject identifiers that are duplicates after trimming. New items begin available. If a saved item is unavailable, its borrow() method provides the existing state change.

An invalid later record may leave a partially built local library, but no library should be returned. The caller’s current library has not been modified. Only a completely reconstructed result can replace it.

Check those decisions before writing the complete reconstruction function.

The next exercise supplies the classes in models.py and the validation helper in main.py. Use your IDE to follow the calls between these files; no data file is needed yet. The Inline Editor is also available for the same checks.

Write library_from_document to call the helper, create a new library, and restore its items through their existing methods.

Run check.py. Its second case has a valid first record followed by an identifier that becomes a duplicate after trimming. Rejection must preserve both the caller’s current library and the input records.

Restore library records

0 / 30 points

Complete library_from_document(data) in main.py, using the supplied classes in models.py. First call the supplied checked_item_records(data) helper. It checks that the document contains an items list of dictionaries with id, name, category, and boolean available fields, then returns those records. Invalid structure raises ValueError. The helper leaves the input unchanged and permits extra fields; use it as supplied. Construct a new EquipmentLibrary and loop over the checked records. Use its add_item operation to validate and trim text and reject identifiers that are duplicates after trimming. Use the item returned by add_item to reconstruct an unavailable item through its borrow() method. Return only a completely reconstructed library. Invalid data raises ValueError; do not print or modify the input. Empty libraries are valid. Identifiers are case-sensitive and names may repeat. The result contains new objects, independent of the input dictionaries. Run check.py to check a successful reconstruction and a valid first record followed by a duplicate identifier after trimming. The rejected attempt must leave the input and the caller’s current library unchanged.

Work in your IDE

An IDE is recommended for this exercise. Download it with the IDE plugin or choose Download starter code in the File Submission tab. Extract the files into a separate exercise folder and open that folder in your IDE, using the environment from Part 5.

From that folder, run py check.py on Windows or python3 check.py on macOS/Linux; use python instead if that is your configured command. Keep models.py, main.py, and check.py together. Follow the reconstruction call into the supplied model; this exercise needs no saved data file. Extend the sample checks with cases from the contract.

Submit with the IDE plugin or upload a ZIP in the File Submission tab. Include the supplied source and sample files and your checks at the archive’s top level. Leave out generated saves, temporary test data, .venv, and __pycache__.

You can also use the Inline Editor tab. Open Show file tree, select check.py, and Run. Run executes the selected file; Submit sends the editor files for grading. Generated files are temporary, so any browser save/load check must happen within one execution.

Keep JSON Operations in the Storage Module

The file exercise supplies the completed classes in models.py and the conversion function in storage.py. Add loading and saving to that storage module, using the JSON operations from Part 5.

Loading has two results to connect: json.load returns plain data, and library_from_document returns the reconstructed library. Saving works in the other direction: the document contains the library’s exported records in its items list.

For saving, assume the library has been kept valid through its model methods. Build {"items": library.to_records()} and write that document with json.dump. Exporting and saving leave the current objects unchanged.

Loading still needs validation: a file may have been edited or damaged outside the program. Pass the data read by json.load to library_from_document and return only the complete reconstructed library. Let file errors reach the caller, which can report them to the user.

The model needs neither JSON operations nor the user interface. The storage module uses the model because it reconstructs model objects. That is the dependency direction from the previous chapter.

Use the supplied check.py for a save/load check. Put the temporary file calls in check.py, which imports the completed operations from storage.py. Keep those calls out of the storage module’s top level.

Work in your IDE for this exercise: inspect a disposable saved JSON file, then load it in a separate run. In the Inline Editor, save and load within the same execution because generated files are temporary.

Keep a library file

0 / 30 points

Complete load_library(path) and save_library(path, library) in storage.py. The exercise supplies models and library_from_document, which checks the saved records and reconstructs a new library.

  • Loading reads one UTF-8 JSON document, passes it to library_from_document, and returns that result. File, parsing, and validation failures propagate.
  • Saving receives a valid library. Export {"items": library.to_records()} and write that document as UTF-8 JSON, replacing previous contents. Return None, without changing the live library. File errors propagate.

Keep file operations inside the requested functions. Importing a module must not read or write files, print, or start interaction. An empty library is valid and replaces old contents. The supplied check.py performs a save/load round trip in one execution.

Work in your IDE

An IDE is recommended for this exercise. Download it with the IDE plugin or choose Download starter code in the File Submission tab. Extract the files into a separate exercise folder and open that folder in your IDE, using the environment from Part 5.

From that folder, run py check.py on Windows or python3 check.py on macOS/Linux; use python instead if that is your configured command. The supplied check saves and loads during one execution. Also use a disposable JSON file to save in one run and load in a later run. Inspect its records before deleting it. Extend the sample checks with cases from the contract.

Submit with the IDE plugin or upload a ZIP in the File Submission tab. Include the supplied source and sample files and your checks at the archive’s top level. Leave out generated saves, temporary test data, .venv, and __pycache__.

You can also use the Inline Editor tab. Open Show file tree, select check.py, and Run. Run executes the selected file; Submit sends the editor files for grading. Generated files are temporary, so any browser save/load check must happen within one execution.

Check Your Understanding

  1. Why does saving an arbitrary instance require an explicit representation decision?
  2. How does to_records differ from returning the internal list?
  3. Which existing rules does reconstruction reuse?
  4. Why can a partially built local result be discarded without changing live state?
  5. Why is successful reconstruction not expected to preserve object identity?