Packaging and environments
The environment is part of the model. A prediction is reproducible only when the code, the weights and every pinned dependency are.
The dependency is the model
A model trained with one version of a library and served with another can change behaviour without a single weight changing. Numerical libraries, tokenisers and even the order of a dictionary can shift a score. The environment is not a detail around the model; it is part of the computation that produces the prediction.
This is why a model card that lists only the algorithm and the data is incomplete. It should also name the Python version, the framework version and the preprocessing library, because those decide what the numbers mean.
Preprocessing is the usual culprit. A tokenizer that lowercases differently, a scaler fitted on a different sample, or a category mapping that gained an entry can change the input before the model sees it. The weights are identical and the prediction is not, which is the hardest kind of regression to explain. The same rule applies to system libraries: a Python package that links against an unpinned libc is only half pinned.
Lock, do not list
A requirements file with direct dependencies is a wish; a lockfile with every transitive package and its hash is a record. Resolve once in CI, build the image from the lock, and promote that image rather than rebuilding it per environment. A rebuild at deploy time is a new environment wearing the old name.
Pinning everything by hand is not the same as locking. The resolver knows about conflicts and platform wheels that a human list does not. Use the resolver, commit its output, and review the diff when it changes.
Lockfiles are platform-specific more often than teams expect. A wheel for macOS and a wheel for Linux can resolve to different transitive sets, and a lock generated on a laptop may not install in the container. Generate the lock in the same image family that will run it, and test the install there.
Read the versions you actually have
from importlib.metadata import version
for package in ["numpy", "pandas", "scikit-learn", "mlflow"]:
print(f"{package}=={version(package)}")Two environments, one model
Run the comparison and identify which difference is most likely to change predictions. Then pin the serving environment to the training versions and confirm the report is empty.
Containers and the base image
The base image is a dependency too. A slim Python image, a fixed digest and no build tools at runtime is the usual shape. The digest matters: a tag such as latest can point at a different image tomorrow, and the rebuild that worked yesterday now starts from a patched operating system.
Keep large weights out of the image when they would make every build slow. Mount them or fetch them by immutable version, and make that fetch a startup check. A service that starts without its weights and answers with a default is worse than one that refuses to start.
Keep build tools out of the runtime image. A compiler, a package manager cache and a test suite add attack surface and size, and none of them helps serve a request. Build in one stage, copy the installed environment into a slim runtime stage, and run as a non-root user.
Pin direct dependencies and lock transitive ones. The first makes intent readable; the second makes the build reproducible.
The reproducibility manifest
Record the Python version, the package versions, the data snapshot and the commit. A manifest turns “it worked on my machine” into a diff. It also makes a rollback possible: you can rebuild the old environment instead of guessing which package changed.
Store the manifest with the artefact, not in a wiki. When an incident asks what was different about Tuesday’s model, the answer should be one file away, and it should be the file the pipeline actually used.
The manifest is also a debugging tool. When two runs disagree, diff their manifests before reading the code. The answer is often a package patch, a different data snapshot or a Python minor version, and the diff finds it in seconds instead of an afternoon. Keep it in a text format a diff can read, not a screenshot of a terminal or a wiki page.
A fingerprint for the data
Run the manifest twice, then change one value in DATA and confirm the hash changes. A manifest that does not cover the data cannot explain a different result.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why is the environment part of the model?
