Two conda artifacts, not one: keeping Anaconda environments reproducible
An environment.yml records what you asked for, not what got installed. Keep a hand-edited request file plus a platform-specific explicit lock, and verify both in a clean prefix.
07 Jul 2026, 16:52 UTC

The environment that resolves differently six months later
You hand a colleague an environment.yml listing six packages. They run conda env create -f environment.yml, it succeeds, and the project still fails on import. Nothing is broken — conda did exactly what the file asked. The file asked for package names, and conda resolved those names against whatever the configured channels offered that day.
That is the distinction worth internalizing: environment.yml is a dependency request, not a record of resolved state. Using one file for both jobs is the most common reproducibility mistake in conda workflows.
The thesis: two artifacts with two different jobs
Keep a human-editable environment.yml that lists what your project actually depends on, and a separate platform-specific lock that pins what was installed. The first is for people reading and editing; the second is for machines rebuilding.
| Artifact | What it records | Typical rebuild command | Portability |
|---|---|---|---|
environment.yml from conda env export --from-history | Packages you explicitly requested through conda | conda env create -f environment.yml | Broad; resolves fresh each time |
Explicit lock from conda list --explicit | Exact versions, build strings, channel URLs | conda create -n NAME --file lock.txt | Same platform and architecture only |
A full conda env export (without --from-history) also produces a pinned, version-and-build-string file, but it is YAML and equally platform-specific. Either form works as the lock; the explicit list is simply harder to edit by accident.
Worked example: capture both artifacts
Run these in a terminal where conda is initialized — conda --version should print something. No elevated privileges are required; you need write access to the environment prefix directory and to the project folder. Replace myenv and project/ with your own names.
- Create the environment with the packages you intend to depend on.
- Export the request file and the resolved lock side by side.
- Commit both to version control.
conda create -n myenv python=3.11 pandas scikit-learn
conda activate myenv
conda env export --from-history > project/environment.yml
conda list --explicit > project/conda-explicit-lock.txt
Expected result: environment.yml contains a short list matching what you typed, and the lock file contains one URL per package with version and build string. Check the YAML before committing — --from-history reflects packages requested through conda in that environment, and channel information may be absent depending on how the packages were requested. Add a channels: entry explicitly if your team needs a fixed channel order.
To rebuild the request: conda env create -f project/environment.yml. To rebuild the exact set on the same platform: conda create -n myenv-locked --file project/conda-explicit-lock.txt.
Where this breaks down
- pip inside conda. Packages installed with pip are not fully captured by conda's solver.
--from-historycan omit them, and the explicit lock will not describe their transitive dependencies. If you must mix, add apip:subsection to the YAML and generate a separate pip lock (for example withpip freeze) alongside it. - Channels.
defaultsandconda-forgecan supply different builds of the same package, and implicit channel priority can change which one wins. Pin channels explicitly and keep the list short. - Platform. Build strings encode OS and architecture. A lock built on Linux will not recreate on macOS or Windows without adjustment, so cross-platform reproducibility needs a lock per target.
- Solver and policy drift. Solver behavior and channel-priority defaults are version-sensitive: the same YAML can resolve differently across conda releases. Anaconda's channel terms have also changed over time — confirm current licensing and terms before standardizing on
defaultsin an organization. Treat both points as items to verify against current documentation rather than assumptions. - Not a security boundary. Neither artifact tells you whether a package is vulnerable or where it came from. Provenance and vulnerability review are separate work.
Verify in a clean prefix, then clean up
A successful solve is not a working environment. Recreate in a fresh prefix or container and run a smoke test:
conda create -n verify-env --file project/conda-explicit-lock.txt
conda activate verify-env
python -c "import pandas, sklearn; print('imports ok')"
conda list > verify-list.txt
Then compare verify-list.txt against the lock, focusing on your direct dependencies and their build strings. If the project has a test suite, run its fastest subset here rather than only checking imports. Repeat on each target OS if you claim cross-platform reproducibility.
Creating environments changes state on disk, so remove the throwaway ones when finished: conda env remove -n verify-env. The same command removes myenv-locked if you created it only to test the lock.
The practical takeaway
Write environment.yml by hand for intent, generate the lock with conda list --explicit for exactness, and let CI rebuild from the lock while humans edit the YAML. When the two disagree, the lock is what actually ran — regenerate it deliberately rather than letting a fresh resolve silently redefine your environment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.