Problem
When you need this. Use a retained content identity when the model loaded for inference must be the same bytes that passed review.
A file called latest.json can change between review and use. After checking the exported candidate against a quality policy, you will register passing bytes under their digest and load them using a separately retained expected version.
Concepts
A content-addressed version names bytes by their hash. A mutable alias is a separate name that can point to different versions over time. The lab requires the explicit digest and does not implement an alias. MLflow also distinguishes versions and aliases, but this exercise builds a small local filesystem registry, not an MLflow service.
The expected digest matters only if the consumer retains it independently of the file being checked. Recomputing the expectation from a changed file accepts that change. Hashes identify bytes; they do not authenticate a publisher, authorize a release or make ordinary storage physically immutable.
Architecture
register() checks the supplied bytes against the expected digest and recomputes the quality gate before creating the digest-named file exclusively. An identical retry succeeds; conflicting bytes are refused. load() reads the selected file once, checks its digest against the retained expectation, then validates its inference contract.
Working code
Download the complete lab, extract it, and enter mlops-registry-serving/. Use Python 3.12 or later; there are no third-party dependencies. The ZIP includes the candidate, data, source and tests.
python exercise.py registry
python -m unittest -v test_pipeline.pyRegister twice without changing the selected version
Run python exercise.py registry. It uses a temporary directory, registers the saved candidate, registers identical bytes again, and loads the selected model. Inspect candidate_sha256 and the accepted identical retry. The expected digest comes from the separately retained run result; it is not recalculated from the registry file during loading.
Change bytes while retaining the old expectation
The exercise appends a space to the temporary registered JSON. The parsed JSON would still have the same values, but its bytes differ. Loading must refuse it. The exercise also requests latest, which is refused because it is not an explicit digest.
All modifications occur in a temporary directory. The bundled candidate and original training records remain unchanged. In your own integration, investigate a partial or conflicting registration rather than overwriting it to make a retry appear successful.

Read the actual pipeline.py implementation
"""Local Rice inference export, validation gate and digest-selected registry.
No external packages. This is a trusted local filesystem exercise, not MLflow.
"""
import hashlib
import json
import math
import re
from pathlib import Path
from vendor import baseline
HERE = Path(__file__).resolve().parent
CONTRACT = 'rice-measurements-v1'
POLICY = {'accuracy': [90, 100], 'class_recall': [85, 100]}
def encode(value):
return (json.dumps(value, sort_keys=True, separators=(',', ':'),
allow_nan=False) + '\n').encode()
def digest(data):
return hashlib.sha256(data).hexdigest()
def pairs(items):
result = {}
for key, value in items:
if key in result:
raise ValueError('duplicate JSON key')
result[key] = value
return result
def decode(data):
return json.loads(data, object_pairs_hook=pairs,
parse_constant=lambda _: (_ for _ in ()).throw(ValueError('nonfinite JSON')))
def number(value):
try:
finite = type(value) in (float, int) and math.isfinite(value)
except OverflowError:
finite = False
if not finite:
raise ValueError('expected a finite non-Boolean number')
return value
def validate(bundle):
if not isinstance(bundle, dict):
raise ValueError('expected a bundle object')
if (type(bundle.get('format')) is not int or bundle.get('format') != 1 or bundle.get('contract') != CONTRACT
or bundle.get('features') != baseline.FEATURES
or bundle.get('labels') != baseline.LABELS
or bundle.get('positive') != baseline.POSITIVE):
raise ValueError('unsupported bundle contract')
for field in ('weights', 'mean', 'std'):
values = bundle.get(field)
if not isinstance(values, list) or len(values) != len(baseline.FEATURES):
raise ValueError('wrong parameter dimensions')
for value in values:
number(value)
if any(value <= 0 for value in bundle['std']):
raise ValueError('nonpositive standard deviation')
number(bundle.get('bias'))
return bundle
def predict(bundle, request, version):
if not isinstance(request, dict) or set(request) != {'contract', 'features'}:
raise ValueError('expected contract and features')
features = request['features']
if (request['contract'] != bundle['contract'] or not isinstance(features, dict)
or set(features) != set(bundle['features'])):
raise ValueError('feature contract mismatch')
values = [number(features[name]) for name in bundle['features']]
scaled = [(v - m) / s for v, m, s in zip(values, bundle['mean'], bundle['std'])]
z = bundle['bias'] + sum(w * v for w, v in zip(bundle['weights'], scaled))
if not all(math.isfinite(v) for v in scaled) or not math.isfinite(z):
raise ValueError('nonfinite computation')
probability = baseline.sigmoid(z)
label = baseline.POSITIVE if z >= 0 else baseline.LABELS[1]
return {'version': version, 'contract': CONTRACT, 'label': label,
'positive_probability': probability}
def request_for(row):
return {'contract': CONTRACT, 'features': dict(zip(baseline.FEATURES, row['x']))}
def export():
parts = baseline.split_rows(baseline.load_rows())
scaler = baseline.fit_scaler(parts['training'])
model = baseline.train_logistic(parts['training'], scaler)
bundle = {'format': 1, 'contract': CONTRACT, 'features': baseline.FEATURES,
'labels': baseline.LABELS, 'positive': baseline.POSITIVE,
'weights': model['weights'], 'bias': model['bias'], **scaler,
'training': {'dataset_sha256': baseline.DATA_SHA256,
'split_salt': baseline.SALT, 'epochs': baseline.EPOCHS,
'learning_rate': baseline.LEARNING_RATE,
'trainer_sha256': digest(Path(baseline.__file__).read_bytes()),
'exporter_sha256': digest(Path(__file__).read_bytes())}}
validate(bundle)
# Match the source predictor on every validation row before retaining bytes.
version = digest(encode(bundle))
for row in parts['validation']:
if predict(bundle, request_for(row), version)['label'] != baseline.predict_logistic(model, scaler, row):
raise ValueError('export prediction mismatch')
return encode(bundle)
def quality_gate(data, expected):
if digest(data) != expected:
raise ValueError('candidate digest mismatch')
bundle = validate(decode(data))
training = bundle.get('training', {})
required = {'dataset_sha256': baseline.DATA_SHA256, 'split_salt': baseline.SALT,
'epochs': baseline.EPOCHS, 'learning_rate': baseline.LEARNING_RATE,
'trainer_sha256': digest(Path(baseline.__file__).read_bytes()),
'exporter_sha256': digest(Path(__file__).read_bytes())}
if training != required:
raise ValueError('unsupported training identity')
rows = baseline.split_rows(baseline.load_rows())['validation']
matrix = {label: {other: 0 for other in baseline.LABELS} for label in baseline.LABELS}
for row in rows:
result = predict(bundle, request_for(row), expected)
matrix[row['label']][result['label']] += 1
correct = sum(matrix[label][label] for label in matrix)
enough = lambda a, b, ratio: b > 0 and a * ratio[1] >= b * ratio[0]
passed = enough(correct, len(rows), POLICY['accuracy']) and all(
enough(matrix[label][label], sum(matrix[label].values()), POLICY['class_recall'])
for label in matrix)
return {'candidate_sha256': expected, 'partition': 'validation', 'policy': POLICY,
'membership_sha256': digest(encode(sorted(row['key'] for row in rows))),
'rows': len(rows), 'correct': correct, 'confusion': matrix, 'passed': passed}
def registered_file(root, version):
if not isinstance(version, str) or not re.fullmatch('[0-9a-f]{64}', version):
raise ValueError('select an explicit SHA-256 version')
root = Path(root)
if root.is_symlink():
raise ValueError('symlink registry root')
return root / (version + '.json')
def register(root, data, expected):
record = quality_gate(data, expected)
if not record['passed']:
raise ValueError('candidate failed quality policy')
path = registered_file(root, expected)
path.parent.mkdir(parents=True, exist_ok=True)
if path.is_symlink():
raise ValueError('symlink registration')
try:
with path.open('xb') as stream:
stream.write(data)
except FileExistsError:
if path.read_bytes() != data:
raise ValueError('conflicting or incomplete registration') from None
return record
def load(root, expected):
path = registered_file(root, expected)
if path.is_symlink():
raise ValueError('symlink registration')
data = path.read_bytes()
if digest(data) != expected:
raise ValueError('registered bytes changed or incomplete')
return validate(decode(data))
def batch(bundle, requests, version):
if not isinstance(requests, list) or not 1 <= len(requests) <= 64:
raise ValueError('batch size must be 1..64')
# Return only a complete list. A bad row raises before any output is returned.
return [predict(bundle, request, version) for request in requests]Read the complete recorded registry output
{
"execution_mode": "local exercise using a previously trained candidate",
"candidate_sha256": "a4692350c5e6d1f231eea8c5eb3354c24276870e73a60922a10abb07abffa5f6",
"first_registration": "accepted",
"identical_retry": "accepted",
"loaded_contract": "rice-measurements-v1",
"changed_bytes": "registered bytes changed or incomplete",
"alias": "select an explicit SHA-256 version"
}The exercise driver shows each operation and cleanup. The fresh-training driver and training run record document how the saved candidate was created. Adding --record to an exercise deliberately replaces its result and generated report; it does not refresh the screenshot automatically.
Failure cases
| Trigger | Required boundary and test |
|---|---|
| Changed or incomplete registration | test_registration_is_idempotent_and_partial_is_refused substitutes a partial file and requires both loading and retry to fail. |
| Alias or symlink substitution | test_alias_and_symlink_refused requires an explicit version and refuses a substituted symlink. |
The focused run records registered bytes changed or incomplete after the whitespace change, and select an explicit SHA-256 version for the alias request.
A crash can leave a partial exclusively created file. This implementation refuses it; it does not provide automatic recovery. The directory and its ancestors are assumed to be trusted during execution. Hostile concurrent edits, ancestor symlink races and distributed transactions are outside this local contract.
Deployment
A consumer loads one expected digest before using the model. Retain that expectation in the consumer’s reviewed configuration, separately from the editable registry file. Do not infer deployment approval from the artifact’s presence; this lesson establishes local selection, not traffic promotion.
Scaling
The local registry keeps one ordinary file per digest and has no remote API, retention manager or distributed coordination. Replacing it with object storage or a registry service requires preserving exact selection and adding that system’s authentication, concurrency and recovery rules. No remote registry has been exercised here.
Key takeaways
A registry file changes. The consumer recomputes its expected digest from that same changed file before loading. What has been lost?
Read the explanation
The expected identity must be retained separately; deriving it from the replacement accepts the replacement.
Best practices
Keep expected identities separate from mutable storage. Make identical retries idempotent, refuse conflicting bytes and investigate incomplete writes. Treat integrity, authorization and release policy as separate checks.
References
- MLflow model-registry workflows explain versions and mutable aliases. This lab uses its own local registry.
- scikit-learn common pitfalls explains fitting preprocessing on training data and applying it consistently during inference.
- Python HTTP server documentation describes the adapter and its production limitations.
- UCI Rice dataset supplies the measurement data under CC BY 4.0.