Problem
When you need this. A job configuration says 300 epochs, but the trainer still calls a function whose default is 200. Saving the JSON would document the intended run while concealing what the program would actually do. An empty package requirements file can be misleading too: this trainer uses the Python standard library, which still comes from a particular interpreter installation.
You will capture the effective setup of the existing supervised baseline. This module returns to the Rice trainer from the first lesson to focus on job setup and execution. Use its bundled seven-measurement CSV; the user-event feature history is a separate data-preparation example. The tool reads the trainer’s source, input bytes and current runtime, then verifies that setup against a selected snapshot. It does not start training or replace the baseline’s recorded metrics.
Concepts
A configuration file states requested settings. Effective configuration is what the program will actually use. Recording a value the trainer ignores would describe a different experiment.
A setup snapshot identifies source, data, configuration, dependency declarations and runtime. It describes the intended execution environment; it is not a completed run or a package installer. Static source inspection reads supported defaults without importing a module that might start training as a side effect.
Architecture
The capture tool parses supported trainer source, reads configuration and data bytes, and records the current interpreter information. Verification compares those components with a selected snapshot and expected identity. It never calls the training function; the next lesson adds process execution and output selection around this setup.
Working code
Record behavior the trainer actually supports
The baseline exposes --final and --out command-line options. Its learning rate, epoch count and split salt are defined in the source. Adding a JSON file does not create a new override interface for those settings.
job-config.json therefore describes the reviewed trainer’s current behavior:
{
"mode": "validation",
"epochs": 200,
"learning_rate": 0.5,
"split_salt": "rice-split-v1",
"positive_class": "Cammeo",
"scaler_fitted_on": "training"
}The capture tool refuses other epoch counts or learning rates. To support those values, first change the trainer’s real interface and review the setup adapter for that source revision. Do not record settings that the invocation will ignore.
Download the complete lab and enter mlops-job-setup/:
python3.12 -m venv .venv
source .venv/bin/activateThe trainer and CSV are unchanged copies from the baseline lesson. The dataset is already bundled, so there is no acquisition or package-install command. Its original results remain with that prerequisite lesson. The lab README retains the UCI Rice dataset attribution and CC BY 4.0 license information.
Read defaults without executing the training module
capture_job.py first requires the supported trainer source fingerprint. It parses the source into an abstract syntax tree, or AST, which represents Python syntax without executing that program. It reads the named constants and the default arguments on train_logistic().
This distinction matters because Python binds default argument values when a function is defined. Assigning a new value to a global afterwards would not update the function’s already-bound default. The adapter checks the actual default references rather than assuming a matching global name is enough.
The same source scan lists the trainer’s direct imports. They are argparse, hashlib, json, math, pathlib, random and tempfile, all from the standard library. A source edit causes the compatibility guard to refuse capture until that trainer revision and adapter are reviewed.
Inspect the source scan, runtime capture and verifier
"""Capture and verify an existing trainer's setup without executing the trainer."""
import argparse
import ast
import hashlib
import json
import platform
from pathlib import Path
import struct
import sys
HERE = Path(__file__).resolve().parent
SUPPORTED_TRAINER_SHA256 = '41b40a9ac8cc6b5b53b45f7b2f696f0fe9488b7fd583da1f20be53b293af97a9'
FILES = ('trainer/baseline.py', 'trainer/rice.csv', 'job-config.json', 'requirements-lock.txt')
def sha256(raw):
return hashlib.sha256(raw).hexdigest()
def identity(value):
raw = json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()
return sha256(raw)
def parse_config(raw):
def pairs(items):
result = {}
for key, value in items:
if key in result:
raise ValueError('duplicate configuration key')
result[key] = value
return result
def constant(value):
raise ValueError('nonstandard JSON constant: ' + value)
return json.loads(raw, object_pairs_hook=pairs, parse_constant=constant)
def trainer_contract(raw):
if sha256(raw) != SUPPORTED_TRAINER_SHA256:
raise ValueError('unsupported trainer revision; review the setup adapter before recapturing')
tree = ast.parse(raw)
names = {'EPOCHS', 'LEARNING_RATE', 'SALT', 'POSITIVE', 'DATA_SHA256'}
constants = {}
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in names:
constants[target.id] = ast.literal_eval(node.value)
function = next(node for node in tree.body if isinstance(node, ast.FunctionDef)
and node.name == 'train_logistic')
# Read the actual definition-time defaults, rather than assuming later global
# assignment would change already-bound Python function defaults.
defaults = {argument.arg: constants[value.id]
for argument, value in zip(function.args.args[-2:], function.args.defaults)
if isinstance(value, ast.Name)}
if defaults != {'learning_rate': constants['LEARNING_RATE'], 'epochs': constants['EPOCHS']}:
raise ValueError('trainer defaults differ from the reviewed adapter')
imports = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.update(alias.name.split('.')[0] for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
imports.add(node.module.split('.')[0])
if not imports <= sys.stdlib_module_names:
raise ValueError('trainer declares imports outside the standard library')
return constants, defaults, sorted(imports)
def describe_runtime():
return {'implementation': platform.python_implementation(),
'python_version': platform.python_version(),
'compiler': platform.python_compiler(),
'system': platform.system(), 'machine': platform.machine(),
'pointer_bits': struct.calcsize('P') * 8}
def capture(root=HERE):
root = Path(root)
raw = {name: (root / name).read_bytes() for name in FILES}
constants, defaults, imports = trainer_contract(raw['trainer/baseline.py'])
config = parse_config(raw['job-config.json'])
expected = {'mode': config.get('mode') if isinstance(config, dict) else None,
'epochs': defaults['epochs'], 'learning_rate': defaults['learning_rate'],
'split_salt': constants['SALT'], 'positive_class': constants['POSITIVE'],
'scaler_fitted_on': 'training'}
if (not isinstance(config, dict) or set(config) != set(expected)
or config['mode'] not in ('validation', 'final')
or type(config['epochs']) is not int
or type(config['learning_rate']) not in (int, float) or config != expected):
raise ValueError('configuration does not describe the existing trainer behavior')
if sha256(raw['trainer/rice.csv']) != constants['DATA_SHA256']:
raise ValueError('dataset differs from the trainer data version')
argv = ['python', 'trainer/baseline.py']
if config['mode'] == 'final':
argv.append('--final')
return {'format': 1, 'mode': 'setup capture only; trainer not executed',
'files': {name: sha256(blob) for name, blob in raw.items()},
'capture_tool_sha256': sha256((HERE / 'capture_job.py').read_bytes()),
'effective_config': config,
'dependencies': {'trainer_direct_imports': imports, 'third_party': []},
'runtime': describe_runtime(), 'planned_argv': argv}
def verify(snapshot, expected_id, root=HERE):
if identity(snapshot) != expected_id:
raise ValueError('snapshot differs from the separately expected identity')
current = capture(root)
if not isinstance(snapshot, dict) or identity(current) != expected_id:
differences = sorted(key for key in set(current) | set(snapshot)
if current.get(key) != snapshot.get(key)) if isinstance(snapshot, dict) else []
raise ValueError('setup differs: ' + (', '.join(differences) or 'canonical values'))
return {'setup_matches': True, 'training_started': False, 'recipe_id': expected_id}
def main():
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest='command', required=True)
save = commands.add_parser('capture')
save.add_argument('--root', type=Path, default=HERE)
save.add_argument('--out', type=Path, required=True)
check = commands.add_parser('verify')
check.add_argument('--root', type=Path, default=HERE)
check.add_argument('--snapshot', type=Path, required=True)
check.add_argument('--expected-id', required=True)
args = parser.parse_args()
if args.command == 'capture':
snapshot = capture(args.root)
with args.out.open('x') as file:
file.write(json.dumps(snapshot, indent=2) + '\n')
print(identity(snapshot))
else:
result = verify(parse_config(args.snapshot.read_bytes()), args.expected_id, args.root)
print(json.dumps(result, indent=2))
if __name__ == '__main__':
main()The selected mode also matters. validation records the planned command python trainer/baseline.py. final adds --final to that plan. Capture does not execute either command; final test evaluation still belongs after settings have been selected and frozen.
Describe the runtime separately from package requirements
A virtual environment isolates a project’s installed packages around an existing base Python installation. Creating .venv does not select an exact interpreter patch version or reconstruct every native dependency. The Python venv documentation recommends recreating environments rather than copying their directories.
This trainer needs no third-party packages. Its requirements-lock.txt states that fact and explicitly leaves the interpreter unpinned. The setup snapshot records runtime fields independently. Your snapshot records the interpreter you use. These are the values observed during the recorded local inspection:
| Field | Recorded value |
|---|---|
implementation | CPython |
python_version | 3.15.0 |
compiler | Clang 21.0.0 (clang-2100.3.34.2) |
system | Darwin |
machine | arm64 |
pointer_bits | 64 |
The platform module provides the implementation, version, compiler, OS family and machine identifiers. The snapshot also records pointer width. It avoids host names and installation paths, which are unnecessary for this comparison.
These descriptors detect changes in the recorded fields. They do not hash the complete Python distribution, the standard library’s transitive dependencies or every native math library. Restoring an environment image is a later packaging concern; matching this descriptor alone is not a promise of bit-for-bit numerical reproduction.
Select a setup snapshot and verify it explicitly
A setup recipe binds the effective configuration, source/data/config file fingerprints, direct import inventory and runtime descriptor. Its ID hashes that captured object. Keep the expected ID from creation separate from the editable snapshot file:
RECIPE_ID=$(python capture_job.py capture --out my-job.json)
python capture_job.py verify --snapshot my-job.json --expected-id "$RECIPE_ID"Run both commands in the same shell. The capture command refuses to overwrite an existing file. Verification captures the current setup again and compares its identity with the selected recipe. Editing the snapshot, changing an input, or changing a captured runtime field causes refusal.
A successful result reports setup_matches: true and training_started: false. It says the checked setup still matches, not that training has run or a model artifact exists. A later job record will need execution status and output identity in separate records.
Exercise drift checks without retraining
The inspection driver verifies the current capture, then edits temporary copies of the configuration, dataset and trainer. A final probe supplies explicitly simulated runtime metadata to test that branch of the verifier. It does not install or claim execution under a second interpreter.
python inspect_setup.py
python -m unittest -v test_setup.pySetup capture only: training_started=False; no new metrics
Trainer imports: argparse, hashlib, json, math, pathlib, random, tempfile
Third-party packages: 0
Effective settings: epochs=200; learning_rate=0.5; mode=validation
Current captured setup verifies: True
configuration: refused=True
dataset: refused=True
trainer: refused=True
simulated runtime metadata: refused=True
Runtime is recorded in result.json; --check compares portable checks only.
Use capture_job.py verify to enforce your own captured runtime and input identity.The 16 tests check refused settings, source/data changes, actual CLI capture and verification, runtime drift, strict configuration parsing and exclusive snapshot creation. The source-change test includes a side effect in a copied trainer; capture refuses that revision without importing or executing it.
Inspect the experiment and portable fixture comparison
"""Exercise setup capture and drift refusal; never start model training."""
import argparse
import copy
import json
from pathlib import Path
import shutil
import tempfile
from unittest.mock import patch
from capture_job import HERE, FILES, capture, verify, identity
def record():
baseline = capture()
matched = verify(baseline, identity(baseline))
probes = []
for name in ('configuration', 'dataset', 'trainer'):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
for file in FILES:
target = root / file
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(HERE / file, target)
if name == 'configuration':
path = root / 'job-config.json'
config = json.loads(path.read_text()); config['epochs'] = 201
path.write_text(json.dumps(config) + '\n')
else:
path = root / ('trainer/rice.csv' if name == 'dataset' else 'trainer/baseline.py')
path.write_bytes(path.read_bytes() + b'\n')
try:
verify(baseline, identity(baseline), root)
except ValueError as error:
probes.append({'changed': name, 'refused': True, 'reason': str(error)})
else:
raise AssertionError('changed setup was accepted')
# An explicitly simulated runtime metadata difference exercises the verifier.
# This does not claim that a second Python interpreter was installed or used.
changed = dict(baseline['runtime'], python_version='simulated-other-version')
with patch('capture_job.describe_runtime', return_value=changed):
try:
verify(baseline, identity(baseline))
except ValueError as error:
probes.append({'changed': 'simulated runtime metadata', 'refused': True, 'reason': str(error)})
else:
raise AssertionError('runtime metadata difference was accepted')
return {'mode': 'local setup inspection; no training, metric computation or model calls',
'snapshot': baseline, 'verified': matched, 'probes': probes}
def portable_checks(result):
"""Check portable behavior; runtime enforcement is tested by verify(), not waived."""
stable = copy.deepcopy(result)
stable['snapshot'].pop('runtime')
stable['verified'].pop('recipe_id') # Derived from the runtime as well as the files.
return stable
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--check', action='store_true')
parser.add_argument('--save', type=Path)
args = parser.parse_args(); result = record()
if args.check and portable_checks(result) != portable_checks(json.loads((HERE / 'result.json').read_text())):
raise SystemExit('portable setup checks differ from the recorded result')
if args.save:
args.save.write_text(json.dumps(result, indent=2) + '\n')
snapshot = result['snapshot']
print('Setup capture only: training_started=False; no new metrics')
print('Trainer imports:', ', '.join(snapshot['dependencies']['trainer_direct_imports']))
print('Third-party packages:', len(snapshot['dependencies']['third_party']))
config = snapshot['effective_config']
print(f"Effective settings: epochs={config['epochs']}; learning_rate={config['learning_rate']}; mode={config['mode']}")
print('Current captured setup verifies:', result['verified']['setup_matches'])
for row in result['probes']:
print(row['changed'] + ': refused=' + str(row['refused']))
print('Runtime is recorded in result.json; --check compares portable checks only.')
print('Use capture_job.py verify to enforce your own captured runtime and input identity.')
if __name__ == '__main__':
main()python inspect_setup.py --check compares portable setup/probe behavior with the bundled record. It excludes the runtime descriptor and its derived recipe ID so the exercise can be checked on another machine. It is not an environment acceptance gate. Use capture_job.py verify against your own selected snapshot to enforce runtime equality.
The complete inspection record contains the observed runtime and refused probe reasons. Next, the MLOps run-identity lesson will attach execution and immutable outputs to a captured setup. It will reference the existing trainer rather than rebuild its learning algorithm.
Design exercise: a new trainer revision adds a real --epochs argument. Identify the adapter and source-identity changes required before capturing a 300-epoch recipe. Then explain which records are still missing after setup verification succeeds, and why an empty third-party requirements list does not identify the full runtime.
Compare your reasoning
Review the changed trainer source and update the setup adapter to read the actual epochs argument and include it in the planned invocation. A 300-epoch recipe is valid only when that invocation will use 300 epochs. Setup verification still leaves execution status, logs and output identity to be recorded. An empty third-party package list also leaves the Python interpreter and its native dependencies to be identified separately.
Failure cases
The epoch mismatch is a setup defect even when the JSON is valid. Source changes and runtime drift also refuse before training. The side-effect test checks that inspecting a trainer cannot execute its module merely to discover defaults.
Check the input and failure boundaries
| Trigger | Required behavior |
|---|---|
| Configuration records settings not used | Refuse a configuration value that differs from the trainer’s effective setting. |
| Unknown or wrong config field | Reject unsupported fields and incorrect value types. |
| Dataset drift | Refuse changed dataset bytes without starting training. |
| Trainer revision or side effect | Reject changed trainer source and inspect it without importing it. |
| Runtime drift | Enforce the selected runtime identity. |
| Edited snapshot replaces expectation | Refuse an edited snapshot against the separately supplied expectation. |
| Snapshot extra/type-alias fields | Reject unknown snapshot fields and boolean/integer type aliases. |
| Configuration bytes differ | Treat changed configuration bytes as a different recipe identity. |
| Dependency declaration differs | Bind the dependency declaration file as part of setup. |
| Duplicate or nonstandard JSON | Reject duplicate fields and nonstandard JSON constants. |
| Portable fixture check confused with environment gate | Separate a portable saved-record comparison from the current-environment gate. |
Deployment
Use setup verification as a preflight step in the job environment that will run the trainer. Prepare dependencies separately using the lab’s documented commands, then capture and select that effective environment. An empty package list for a standard-library trainer does not remove the interpreter from its runtime contract.
Scaling
Source and input fingerprinting add preflight reads rather than training work. Cache only immutable identities whose byte contract is defined. More trainers need explicit supported configuration adapters; guessing settings from arbitrary Python code would turn a bounded inspector into an unreliable program-analysis tool.
Key takeaways
- Requested and effective configuration can differ.
- A setup snapshot precedes execution and cannot prove that training completed.
- Source inspection can avoid import-time side effects.
Read the explanation
Recording an ignored setting would misdescribe execution. Supporting 300 epochs requires a real trainer interface and a reviewed adapter.
Best practices
- Verify setup in the actual job environment.
- Select expected snapshot identity separately from editable files.
- Add supported settings deliberately rather than silently accepting unused configuration.
References
- Python, venv: base interpreters, package isolation and recreating environments.
- Python, platform: runtime identification fields.
- Python, default argument values: defaults are evaluated when the function is defined.
Sources checked October 24, 2026. This lesson records local setup inspection, without a new training run.