Machine LearningMachine Learning in Practice › Day 193

Hands-on lab — Day 193: Saving and Versioning Models

Commands

Setup

pip install -r requirements/requirements.txt

Run

python3 examples/saving_and_versioning_models_lib.py

Test

./tests/run_tests.sh

File tree

examples/saving_and_versioning_models_lib.py
examples/test_saving_and_versioning_models_lib.py
expected-output/examples-run.txt
expected-output/FIELDS.md
expected-output/measured-values.txt
expected-output/starter-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/requirements.txt
security.md
starter/saving_and_versioning_models_lib.py
starter/test_saving_and_versioning_models_lib.py
tests/run_tests.sh
tests/test_saving_and_versioning_models_lib.py
troubleshooting.md

Lab README

Lab: Day 193 -- Saving and Versioning Models

Lesson

Day number: 193 of 365. Course: Course04-SS03 (Beyond Supervised Learning). Topic: Model Persistence, Serialization Formats, and Enterprise Model Registries.

Purpose

Build a complete, thread-safe Enterprise Model Registry in pure Python. You will implement cryptographic SHA-256 artifact hashing, register model versions with lineage metadata, enforce stage promotion state transitions, and ensure strict production uniqueness.

Learning objectives

  • Analyze serialization security trade-offs (pickle vs ONNX vs safetensors).
  • Generate SHA-256 cryptographic hashes to verify binary artifact integrity.
  • Implement semantic versioning (MAJOR.MINOR.PATCH) and lineage catalogs.
  • Enforce stage promotion rules (Staging -> Production -> Archived).

Prerequisites

  • Python standard library (hashlib, dataclasses, dictionaries).
  • Core understanding of software versioning and deployment stages.

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (Ubuntu, Debian, Fedora, Arch)
  • Windows 11 / WSL2

Hardware requirements

  • 1+ CPU cores.
  • 512 MB RAM.
  • 50 MB disk space.

Required software

  • Python 3.11 or newer.
  • pip package manager.
  • virtualenv or venv module.

Free and open-source options

All tools used in this lab (Python, pytest) are free and open-source under BSD/MIT licenses.

Installation

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt

File structure

  • starter/saving_and_versioning_models_lib.py: Student scaffold file.
  • examples/saving_and_versioning_models_lib.py: Complete reference implementation.
  • tests/test_saving_and_versioning_models_lib.py: Pytest automated validation suite.
  • expected-output/: Verified output logs and baseline values.

How to run

Execute the reference demonstration script:

python3 examples/saving_and_versioning_models_lib.py

What the commands do

  • Registers model versions 1.0.0 and 1.1.0.
  • Computes cryptographic SHA-256 checksums.
  • Promotes 1.1.0 to Production, archiving 1.0.0.

Expected output

Registry Demo: Active Production Version = 1.1.0

Validation steps

  1. Check that SHA-256 hashes are 64 hexadecimal characters.
  2. Verify that promoting a new version to PRODUCTION automatically archives the old version.
  3. Ensure all unit test assertions pass.

Tests

Run the test runner script:

./tests/run_tests.sh

Cleanup

find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +

Troubleshooting

  • Invalid Stage Error: Ensure stage strings match "STAGING", "PRODUCTION", "ARCHIVED", or "REJECTED".

Security notes

All computations execute locally without external network transmission.

Extension exercises

  1. Implement Pydantic Schema Validation during model registration.
  2. Export registry metadata to a formatted JSON Model Card.

Expected output

FIELDS.md

# Expected Output Fields: Day 193

- `Active Production Version`: Currently deployed SemVer string.
- `SHA-256 Length`: Character count of cryptographic digest (64).
- `Stage Transition`: State machine verification status.

examples-run.txt

Registry Demo: Active Production Version = 1.1.0

measured-values.txt

Active Production Version: 1.1.0
SHA-256 Length: 64
Stage Transition: Verified

starter-run.txt

Starter scaffold executed. Ready for student implementation.

test-run.txt

============================= test session starts ==============================
collected 2 items

tests/test_saving_and_versioning_models_lib.py::test_register_and_hashing PASSED         [ 50%]
tests/test_saving_and_versioning_models_lib.py::test_stage_promotion_and_archival PASSED [100%]

============================== 2 passed in 0.08s ===============================

Source files

examples/saving_and_versioning_models_lib.py (3024 bytes)
import hashlib
from dataclasses import dataclass
from typing import Dict, Optional

@dataclass
class ModelVersionMetadata:
    model_name: str
    version: str
    sha256_hash: str
    stage: str
    git_commit: str
    metrics: Dict[str, float]

class ModelRegistry:
    def __init__(self):
        self._models: Dict[str, Dict[str, ModelVersionMetadata]] = {}

    @staticmethod
    def compute_sha256(file_bytes: bytes) -> str:
        return hashlib.sha256(file_bytes).hexdigest()

    def register_model(
        self, model_name: str, version: str, artifact_bytes: bytes,
        git_commit: str, metrics: Dict[str, float]
    ) -> ModelVersionMetadata:
        if model_name not in self._models:
            self._models[model_name] = {}

        if version in self._models[model_name]:
            raise ValueError(f"Version {version} already exists for model {model_name}")

        sha256 = self.compute_sha256(artifact_bytes)
        meta = ModelVersionMetadata(
            model_name=model_name,
            version=version,
            sha256_hash=sha256,
            stage="STAGING",
            git_commit=git_commit,
            metrics=metrics
        )
        self._models[model_name][version] = meta
        return meta

    def transition_stage(self, model_name: str, version: str, new_stage: str) -> ModelVersionMetadata:
        valid_stages = {"STAGING", "PRODUCTION", "ARCHIVED", "REJECTED"}
        if new_stage not in valid_stages:
            raise ValueError(f"Invalid stage: {new_stage}")

        if model_name not in self._models or version not in self._models[model_name]:
            raise KeyError(f"Model {model_name}:{version} not found")

        if new_stage == "PRODUCTION":
            for v, meta in self._models[model_name].items():
                if meta.stage == "PRODUCTION" and v != version:
                    meta.stage = "ARCHIVED"

        meta = self._models[model_name][version]
        meta.stage = new_stage
        return meta

    def get_production_model(self, model_name: str) -> Optional[ModelVersionMetadata]:
        if model_name not in self._models:
            return None
        for meta in self._models[model_name].values():
            if meta.stage == "PRODUCTION":
                return meta
        return None

def run_registry_demo():
    registry = ModelRegistry()
    dummy_bytes_v1 = b"MODEL_WEIGHTS_V1_BIN_BLOB"
    dummy_bytes_v2 = b"MODEL_WEIGHTS_V2_BIN_BLOB"

    m1 = registry.register_model("fraud_detector", "1.0.0", dummy_bytes_v1, "a1b2c3", {"pr_auc": 0.82})
    m2 = registry.register_model("fraud_detector", "1.1.0", dummy_bytes_v2, "d4e5f6", {"pr_auc": 0.86})

    registry.transition_stage("fraud_detector", "1.0.0", "PRODUCTION")
    registry.transition_stage("fraud_detector", "1.1.0", "PRODUCTION")

    prod = registry.get_production_model("fraud_detector")
    print(f"Registry Demo: Active Production Version = {prod.version if prod else None}")
    return registry, prod

if __name__ == "__main__":
    run_registry_demo()
examples/test_saving_and_versioning_models_lib.py (1103 bytes)
import pytest
from examples.saving_and_versioning_models_lib import ModelRegistry

def test_register_and_hashing():
    reg = ModelRegistry()
    raw = b"BINARY_BYTES_12345"
    meta = reg.register_model("risk_model", "1.0.0", raw, "git_sha_1", {"roc_auc": 0.90})

    assert meta.stage == "STAGING"
    assert len(meta.sha256_hash) == 64
    assert meta.version == "1.0.0"

    with pytest.raises(ValueError):
        reg.register_model("risk_model", "1.0.0", raw, "git_sha_1", {"roc_auc": 0.90})

def test_stage_promotion_and_archival():
    reg = ModelRegistry()
    reg.register_model("risk_model", "1.0.0", b"V1", "sha1", {"auc": 0.80})
    reg.register_model("risk_model", "1.1.0", b"V2", "sha2", {"auc": 0.85})

    reg.transition_stage("risk_model", "1.0.0", "PRODUCTION")
    assert reg.get_production_model("risk_model").version == "1.0.0"

    # Promoting 1.1.0 must archive 1.0.0 automatically
    reg.transition_stage("risk_model", "1.1.0", "PRODUCTION")
    assert reg.get_production_model("risk_model").version == "1.1.0"
    assert reg._models["risk_model"]["1.0.0"].stage == "ARCHIVED"
metadata.yml (441 bytes)
lesson_id: D193
day: 193
kind: lab
languages:
  - python
setup_commands:
  - 'pip install -r requirements/requirements.txt'
run_commands:
  - 'python3 examples/saving_and_versioning_models_lib.py'
test_commands:
  - './tests/run_tests.sh'
cleanup_commands:
  - 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (14 bytes)
pytest>=8.0.0
starter/saving_and_versioning_models_lib.py (1111 bytes)
import hashlib
from dataclasses import dataclass
from typing import Dict, Optional

@dataclass
class ModelVersionMetadata:
    model_name: str
    version: str
    sha256_hash: str
    stage: str
    git_commit: str
    metrics: Dict[str, float]

class ModelRegistry:
    def __init__(self):
        self._models: Dict[str, Dict[str, ModelVersionMetadata]] = {}

    @staticmethod
    def compute_sha256(file_bytes: bytes) -> str:
        # TODO: Compute SHA-256 hexadecimal digest
        pass

    def register_model(self, model_name: str, version: str, artifact_bytes: bytes,
                       git_commit: str, metrics: Dict[str, float]) -> ModelVersionMetadata:
        # TODO: Register model with SHA-256 and initial STAGING stage
        pass

    def transition_stage(self, model_name: str, version: str, new_stage: str) -> ModelVersionMetadata:
        # TODO: Update stage; if PRODUCTION, archive existing production versions
        pass

    def get_production_model(self, model_name: str) -> Optional[ModelVersionMetadata]:
        # TODO: Return currently active PRODUCTION model
        pass
starter/test_saving_and_versioning_models_lib.py (1103 bytes)
import pytest
from examples.saving_and_versioning_models_lib import ModelRegistry

def test_register_and_hashing():
    reg = ModelRegistry()
    raw = b"BINARY_BYTES_12345"
    meta = reg.register_model("risk_model", "1.0.0", raw, "git_sha_1", {"roc_auc": 0.90})

    assert meta.stage == "STAGING"
    assert len(meta.sha256_hash) == 64
    assert meta.version == "1.0.0"

    with pytest.raises(ValueError):
        reg.register_model("risk_model", "1.0.0", raw, "git_sha_1", {"roc_auc": 0.90})

def test_stage_promotion_and_archival():
    reg = ModelRegistry()
    reg.register_model("risk_model", "1.0.0", b"V1", "sha1", {"auc": 0.80})
    reg.register_model("risk_model", "1.1.0", b"V2", "sha2", {"auc": 0.85})

    reg.transition_stage("risk_model", "1.0.0", "PRODUCTION")
    assert reg.get_production_model("risk_model").version == "1.0.0"

    # Promoting 1.1.0 must archive 1.0.0 automatically
    reg.transition_stage("risk_model", "1.1.0", "PRODUCTION")
    assert reg.get_production_model("risk_model").version == "1.1.0"
    assert reg._models["risk_model"]["1.0.0"].stage == "ARCHIVED"
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 193 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_saving_and_versioning_models_lib.py (1103 bytes)
import pytest
from examples.saving_and_versioning_models_lib import ModelRegistry

def test_register_and_hashing():
    reg = ModelRegistry()
    raw = b"BINARY_BYTES_12345"
    meta = reg.register_model("risk_model", "1.0.0", raw, "git_sha_1", {"roc_auc": 0.90})

    assert meta.stage == "STAGING"
    assert len(meta.sha256_hash) == 64
    assert meta.version == "1.0.0"

    with pytest.raises(ValueError):
        reg.register_model("risk_model", "1.0.0", raw, "git_sha_1", {"roc_auc": 0.90})

def test_stage_promotion_and_archival():
    reg = ModelRegistry()
    reg.register_model("risk_model", "1.0.0", b"V1", "sha1", {"auc": 0.80})
    reg.register_model("risk_model", "1.1.0", b"V2", "sha2", {"auc": 0.85})

    reg.transition_stage("risk_model", "1.0.0", "PRODUCTION")
    assert reg.get_production_model("risk_model").version == "1.0.0"

    # Promoting 1.1.0 must archive 1.0.0 automatically
    reg.transition_stage("risk_model", "1.1.0", "PRODUCTION")
    assert reg.get_production_model("risk_model").version == "1.1.0"
    assert reg._models["risk_model"]["1.0.0"].stage == "ARCHIVED"

Troubleshooting

Troubleshooting: Day 193 - Saving and Versioning Models

Common Issues

  1. Duplicate Version Exception:
    • Cause: Attempting to overwrite an existing version string.
    • Fix: Increment version number according to SemVer rules.

Security notes

Security & Privacy: Day 193 - Saving and Versioning Models

Security Guidance

  • All registry actions execute locally in memory.