Skip to content
VLSI Mentor

Python Part I · Module 16 · Object-Oriented Programming

From Loose Variables to One Structured Result

Thirteen chapters of object-oriented machinery have arrived without anyone asking whether a flow script needs it, and that question deserves an honest answer rather than an assumption. This chapter takes one entirely ordinary thing — the result of running one test — and follows it through four representations, every one of which is already available to you from Modules 6 to 8. Each is written, run, and then broken in the specific way it breaks, because the argument for a better structure is only as good as the failure it prevents.

Intermediate16 min readPythonPart IFundamentalsOOPData Modelling

Part I · Module 16 · Chapter 16.14 · From Loose Variables to One Structured Result

1. One Result, Four Shapes

A test run produces four facts. Here is the progression this chapter walks:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# name, seed, ok, runtime
# -> (name, seed, ok, runtime)
# -> {"name": ..., "seed": ..., "ok": ..., "runtime_s": ...}
# -> {"test": {...}, "timing": {...}}

Every one of these is valid Python you already know. The question is not which is correct — all four work — but where each one stops scaling.

Four steps in sequence. One: separate variables, which fail because a four-argument call has an order nobody enforces. Two: a tuple, which fails because index two is unreadable and reordering changes every meaning. Three: a dictionary, which is readable but where a typed key is a KeyError found only at run time. Four: a nested dictionary, which has a shape nobody declared anywhere.Where each shape stops working01SEPARATE VARIABLESfour arguments, order unenforced02A TUPLEr[2] means nothing to a reader03A DICTIONARYreadable, but a typo is a KeyError04A NESTED DICTIONARYa shape nobody declared
Figure 1 — the four representations, and the failure that ends each one. Nothing here is wrong; each simply stops being adequate at a different point, and the last column is what Chapter 16.15 exists to fix.

2. Separate Variables

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def summarise(name, seed, ok, runtime_s):
    status = "PASS" if ok else "FAIL"
    return f"{name}/{seed} {status} {runtime_s}s"


print(summarise("smoke", 1234, True, 182.4))
print(summarise("cdc", 5678, False, 204.1))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke/1234 PASS 182.4s
cdc/5678 FAIL 204.1s

Fine for one function. The problem appears the moment the result travels:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def summarise(name, seed, ok, runtime_s):
    status = "PASS" if ok else "FAIL"
    return f"{name}/{seed} {status} {runtime_s}s"


def load():
    return "smoke", 1234, True, 182.4


name, seed, ok, runtime_s = load()

print("correct:", summarise(name, seed, ok, runtime_s))
print("swapped:", summarise(name, runtime_s, ok, seed))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
correct: smoke/1234 PASS 182.4s
swapped: smoke/182.4 PASS 1234s

The second call is wrong and raised nothing. seed and runtime_s are both numbers, so swapping them produces a plausible sentence with the seed in the runtime column.

Every function that handles a result now needs four parameters, in an order nobody enforces, and adding a fifth field means editing every signature.

3. A Tuple

Grouping them at least makes the result one value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def load():
    return ("smoke", 1234, True, 182.4)


def summarise(result):
    status = "PASS" if result[2] else "FAIL"
    return f"{result[0]}/{result[1]} {status} {result[3]}s"


r = load()
print("one value :", r)
print("summary   :", summarise(r))
print("unpacked  :", "name, seed, ok, runtime = r")
name, seed, ok, runtime = r
print("           ", name, seed, ok, runtime)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
one value : ('smoke', 1234, True, 182.4)
summary   : smoke/1234 PASS 182.4s
unpacked  : name, seed, ok, runtime = r
            smoke 1234 True 182.4

One value travels, and unpacking (Chapter 11.9) restores readable names. But result[2] inside summarise means nothing to a reader, and the real failure is what happens when the shape changes:

Azvya Education Pvt. Ltd.VLSI Mentor
reorder.py (BROKEN ON PURPOSE)
def load():
    """A 'status' field was inserted in the middle by a colleague."""
    return ("smoke", 1234, "TIMEOUT", True, 182.4)


def summarise(result):
    status = "PASS" if result[2] else "FAIL"
    return f"{result[0]}/{result[1]} {status} {result[3]}s"


print(summarise(load()))

name, seed, ok, runtime = load()
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke/1234 PASS Trues
Traceback (most recent call last):
  File "reorder.py", line 13, in <module>
    name, seed, ok, runtime = load()
    ^^^^^^^^^^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 4)

summarise printed garbage and did not raise. result[2] is now the string "TIMEOUT", which is truthy, so it reported PASS for a timed-out test — and result[3] is the boolean, printed as Trues.

The unpacking on the last line did raise, which is the only reason anybody would find out. A tuple's meaning is positional, so inserting a field silently reinterprets every index after it.

4. A Dictionary

Names instead of positions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def load():
    return {"name": "smoke", "seed": 1234, "ok": True, "runtime_s": 182.4}


def summarise(r):
    status = "PASS" if r["ok"] else "FAIL"
    return f"{r['name']}/{r['seed']} {status} {r['runtime_s']}s"


r = load()
print("summary:", summarise(r))

r["status"] = "TIMEOUT"          # a new field, inserted
print("after adding a field:", summarise(r))
print("keys:", sorted(r))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
summary: smoke/1234 PASS 182.4s
after adding a field: smoke/1234 PASS 182.4s
keys: ['name', 'ok', 'runtime_s', 'seed', 'status']

Adding a field broke nothing, which is the headline improvement. Each access names what it wants, so new fields are invisible to code that does not use them.

This is genuinely good, and a large amount of production Python stops here. The two things it cannot do:

Azvya Education Pvt. Ltd.VLSI Mentor
typo.py (BROKEN ON PURPOSE)
def load():
    return {"name": "smoke", "seed": 1234, "ok": True, "runtime_s": 182.4}


def summarise(r):
    status = "PASS" if r["ok"] else "FAIL"
    return f"{r['name']}/{r['seed']} {status} {r['runtime']}s"


print("about to summarise")
print(summarise(load()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
about to summarise
Traceback (most recent call last):
  File "typo.py", line 11, in <module>
    print(summarise(load()))
          ~~~~~~~~~^^^^^^^^
  File "typo.py", line 7, in summarise
    return f"{r['name']}/{r['seed']} {status} {r['runtime']}s"
                                               ~^^^^^^^^^^^
KeyError: 'runtime'

A typo is a runtime error, found only when that line executes — which for a reporting branch may be during the one failing nightly you needed it for.

And a missing field is the same error from the other direction:

Azvya Education Pvt. Ltd.VLSI Mentor
missing.py (BROKEN ON PURPOSE)
def load_incomplete():
    """The parser could not determine a runtime, so it omitted the key."""
    return {"name": "smoke", "seed": 1234, "ok": True}


def summarise(r):
    status = "PASS" if r["ok"] else "FAIL"
    return f"{r['name']}/{r['seed']} {status} {r['runtime_s']}s"


print(summarise(load_incomplete()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Traceback (most recent call last):
  File "missing.py", line 11, in <module>
    print(summarise(load_incomplete()))
          ~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "missing.py", line 8, in summarise
    return f"{r['name']}/{r['seed']} {status} {r['runtime_s']}s"
                                               ~^^^^^^^^^^^^^
KeyError: 'runtime_s'

The dictionary was constructed successfully with three of four fields. Nothing objected at creation; the gap surfaced one function later.

5. A Nested Dictionary

Results grow, and grouping related fields is the natural next move:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def load():
    return {
        "test": {"name": "smoke", "seed": 1234},
        "outcome": {"ok": True},
        "timing": {"runtime_s": 182.4, "queued_s": 12.0},
    }


def summarise(r):
    status = "PASS" if r["outcome"]["ok"] else "FAIL"
    return f"{r['test']['name']}/{r['test']['seed']} {status} {r['timing']['runtime_s']}s"


print(summarise(load()))
print("top-level keys:", sorted(load()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke/1234 PASS 182.4s
top-level keys: ['outcome', 'test', 'timing']

It works, and something has quietly gone wrong: there is now a format, and it is written down nowhere.

Azvya Education Pvt. Ltd.VLSI Mentor
nested.py (BROKEN ON PURPOSE)
def load_old_format():
    """An older producer put runtime at the top level."""
    return {
        "test": {"name": "smoke", "seed": 1234},
        "outcome": {"ok": True},
        "runtime_s": 182.4,
    }


def summarise(r):
    status = "PASS" if r["outcome"]["ok"] else "FAIL"
    return f"{r['test']['name']}/{r['test']['seed']} {status} {r['timing']['runtime_s']}s"


print(summarise(load_old_format()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Traceback (most recent call last):
  File "nested.py", line 15, in <module>
    print(summarise(load_old_format()))
          ~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "nested.py", line 12, in summarise
    return f"{r['test']['name']}/{r['test']['seed']} {status} {r['timing']['runtime_s']}s"
                                                               ~^^^^^^^^^^
KeyError: 'timing'

Read what survived before the failure. The outcome and test lookups both worked, because this producer supplies those; the status line completed normally. What is missing is the timing group — this producer put runtime_s at the top level instead — and the caret sits precisely under r['timing'], the one subscript of three on that line that failed.

Nothing here is a typo. Two producers disagree about a structure that neither of them declares anywhere, and the only record of the expected shape is the subscripts in the consumer.

6. Where It Hurts, Precisely

An honest summary, because a dict is good enough for a great deal of real work:

ShapeTravels as one valueSelf-describingNew field is safeMissing field found
separate variablesnonono — every signature changesat the call, silently wrong
tupleyesnono — every index shiftsat unpack, if you unpack
dictyesat each useyesat first use, as KeyError
nested dictyesat each useyesat first use, two levels deep

The dict fixed almost everything. What remains is two specific gaps:

  1. The fields are not stated anywhere. No reader, editor or tool can list what a result contains without reading every producer. The shape exists only as a habit.
  2. A missing field is found at first use, not at construction. The object that is wrong and the line that fails are in different functions, which is Chapter 14.2 Section 5's problem by design rather than by accident.

Neither is solved by trying harder. Both are solved by naming the structure and declaring its fields, which is exactly what the next chapter does — and the reason it arrives now rather than at the start of the module.

7. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
loose = ("smoke", 1234, True, 182.4)
named = {"name": "smoke", "seed": 1234, "ok": True, "runtime_s": 182.4}

print("A:", loose[1], named["seed"])
print("B:", len(loose), len(named))

grown_tuple = loose + ("TIMEOUT",)
grown_dict = dict(named)
grown_dict["status"] = "TIMEOUT"

print("C:", grown_tuple[1], grown_dict["seed"])
print("D:", "seed" in named, 1234 in loose)
print("E:", sorted(named) == sorted(grown_dict))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 1234 1234
B: 4 4
C: 1234 1234
D: True True
E: False

A and C — both shapes survived a field being appended. Appending to a tuple is safe; Section 3's failure needed an insertion, which is the case nobody controls.

D is the asymmetry. "seed" in named asks about a field; 1234 in loose asks about a value and happens to be true because that number is in there. A tuple cannot be asked whether it has a seed at all.

E confirms the dict grew a key. There is no equivalent question to ask of the tuple — len went from 4 to 5 and the meaning of index 4 is whatever the author remembers.

8. Exercises

Exercise 1 — four parameters. Write a function taking four loose values and call it with two of them swapped. Confirm the result is wrong and silent.

Exercise 2 — the tuple insert. Insert a field into the middle of a returned tuple without changing the consumer. Record both what printed and what raised.

Exercise 3 — index to name. Convert a tuple-based function to a dict-based one. Count the places the change was needed.

Exercise 4 — the typo. Misspell a dict key in a branch that only runs on failure. Say when you would discover it.

Exercise 5 — the missing field. Build a result dict with one field omitted. Show that construction succeeds and name the function where it fails.

Exercise 6 — two producers. Write two functions returning the same data in differently nested shapes. Feed both to one consumer and record what happens.

Exercise 7 — the table. Reproduce Section 6's table from your own experiments rather than from the page.

Exercise 8 — state the fields. Without using any new syntax, write down somewhere in your program what fields a result has. Say why a comment is not quite enough.

9. Summary

  • One result can be carried four ways, all of them already available from Modules 6 to 8.
  • Separate variables do not travel: every function needs four parameters in an order nobody enforces, and swapping two of the same type is silent.
  • A tuple travels as one value but is positional, so inserting a field silently reinterprets every index after it.
  • Tuple failures are usually silent; only unpacking the wrong count raises.
  • A dict is readable and tolerant of new fields, which is why a great deal of real Python stops here.
  • A dict's weaknesses are specific: a typo'd key is a runtime KeyError, and a missing field is found at first use rather than at construction.
  • A nested dict adds a format nobody declared, and two producers can disagree about it without either being obviously wrong.
  • The caret line tells you which nested lookup failed when a key error is two levels deep.
  • The two remaining gaps are stating the fields and failing at construction — and neither is fixed by discipline.

The next chapter closes both gaps with four lines.

Where this fits

Part of the Python curriculum.