aiml
SearchMy learning
Menu

Lesson 9 of 19Module 3

Capture the training configuration

Record the trainer’s actual settings, inputs, dependencies and runtime.

Check whether a declared 300-epoch recipe would actually run for 300 epochs. Capture the inputs and Python runtime without launching training.

Prerequisites

The reproducible supervised baseline and data/schema/feature versioning lessons; Python function defaults and basic command-line use.

Preparation and term definitions

By the end, you can

  • Check that recorded configuration describes the trainer's effective behavior.
  • Capture direct dependencies separately from the Python runtime that provides them.
  • Verify a selected setup without confusing preflight success with completed training.
Runtime
Python 3.12+
Dependencies
Standard library only

Lab code, tests and fixtures (ZIP, 182 KB)Quiz check your understanding

Progress is saved in this browser.

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

Reviewed trainer and databaseline.pyrice.csvDeclared configurationjob-config.jsonCurrent Python runtimeimplementation and versioncompiler, system and machineCapture the setupread defaults and importsbind files and runtime fieldsJob setupsnapshotsource + config + dataruntime + importsVerification captures the current setup againand compares it with the selected snapshot ID.Reviewed trainer and databaseline.py + rice.csvDeclared configurationjob-config.jsonCurrent Python runtimeimplementation and versioncompiler, system and machineCapture the setupread defaults and importsbind files and runtime fieldsJob setup snapshotsource + config + dataruntime + imports
The captured object describes setup, not a completed training run. The tool reads the supported trainer's source syntax and input bytes; there is no training stage in this workflow.

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:

job-config.jsonSource · jsonDownload
{
  "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/:

Prepare the local setup-capture exerciseTerminal · bash
python3.12 -m venv .venv
source .venv/bin/activate

The 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_job.pySource · pythonDownload
"""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:

FieldRecorded value
implementationCPython
python_version3.15.0
compilerClang 21.0.0 (clang-2100.3.34.2)
systemDarwin
machinearm64
pointer_bits64

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:

Capture and verify your current setupTerminal · bash
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.

Run the setup-drift experiment and offline testsTerminal · bash
python inspect_setup.py
python -m unittest -v test_setup.py
Recorded local setup inspectionRecorded output · textDownload
Setup 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
inspect_setup.pySource · pythonDownload
"""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.
job-config.json requests 300 epochs, but this supported trainer uses its 200-epoch default. What should setup capture do?

Choose one answer, then check your reasoning.

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

Sources checked October 24, 2026. This lesson records local setup inspection, without a new training run.

Written by Arjun Prabhulal.

Module 3

Run and track training