DVC — Data Version Control
Version datasets like code. DVC pipelines, remote storage, experiment tracking, and the full DVC + Git workflow for reproducible ML projects.
Your model code is in Git. Your training data is on someone's laptop, or in an S3 bucket with no version history, or in a folder called data_final_v3_use_this. DVC fixes this.
Git tracks code beautifully — every change, every author, every commit. But Git breaks for large files. A 2GB training CSV committed to Git bloats the repository, slows every clone, and makes every checkout painful. More importantly, Git does not understand that a CSV file and the Python script that produced it are connected — if the script changes, Git does not know the CSV is now stale.
DVC (Data Version Control) adds data and model versioning on top of Git. It stores large files in remote storage (S3, GCS, Azure Blob) and keeps tiny pointer files in Git — a .dvc file that is just a hash and a path. When you git checkout an old branch, DVC knows which version of the data that branch used and pulls it from remote storage. Every model in your history has a corresponding dataset version, a feature pipeline version, and a code version. Reproduce any past experiment with two commands.
Git is like a library catalogue — it tracks which books exist and where they are filed, but the books themselves are stored on shelves. DVC is the cataloguing system for your ML datasets — it tracks which version of your data exists and stores a reference in Git, while the actual data lives in a remote storage warehouse (S3). When you need the book (data), you check the catalogue (Git + DVC), find the shelf (S3 path), and retrieve it. The catalogue is tiny. The warehouse can hold terabytes.
The .dvc pointer file committed to Git is typically 200 bytes. The actual dataset it references can be 200GB. Git stores the pointer. S3 stores the data. DVC coordinates between them so every git checkout brings the right data version automatically.
Cache, remote, .dvc files — the three pieces that make versioning work
DVC pipelines — define stages, dependencies, and outputs so reruns are smart
Tracking individual files is useful but DVC pipelines go further. A pipeline defines each processing stage — what inputs it depends on, what command it runs, what outputs it produces. DVC tracks all of these and only reruns a stage when its inputs have changed. If your feature engineering script has not changed and the raw data has not changed, dvc repro skips that stage entirely. The entire ML workflow becomes a reproducible, incremental build system — like Make but for data.
Complete DVC pipeline — four Python scripts driven by params.yaml
dvc exp — run, compare, and select the best experiment without leaving the terminal
DVC experiments extend the pipeline with a lightweight experiment tracking layer. Run an experiment with modified parameters without creating a new Git commit — DVC saves the experiment as a stash. After running several experiments, compare them in a table, pick the best one, and promote it to a full Git commit. This integrates with MLflow and W&B (Module 70) for richer visualisations while keeping the experiment lineage in Git.
The complete Git + DVC daily workflow for an ML team
Every common DVC mistake — explained and fixed
How data versioning actually plays out on an ML team
In practice, DVC rarely versions the raw firehose of events. At a company like DoorDash or Stripe, raw orders, transactions, and clickstream events live in a warehouse (Snowflake, BigQuery, Redshift) that already has its own retention and query history. What actually goes through DVC is the extracted, feature-engineered training snapshot — the parquet file a data engineer produces by querying the warehouse as of a specific date, that a training job then reads directly. That snapshot is the artifact whose exact bytes need to be reproducible, auditable, and tied to a Git commit — which is precisely the problem DVC is built to solve.
The day-to-day workflow splits across roles. A data or ML engineer owns the extraction script and runs dvc add after it produces a new snapshot. Code reviewers on the pull request look at the diff of the .dvc file itself — a changed MD5 hash and a changed row count are usually the only signal a reviewer needs to ask why the data changed, and whether it was intentional. CI does not re-download every historical dataset version on every PR; it runs dvc pull to fetch only the version pinned by that branch's commit, then dvc repro to confirm the pipeline still produces the same metrics on that exact data.
This is the entire point of the setup: a postmortem question that would otherwise be answered with "we think it was probably the March data, roughly" becomes a five-command lookup with an exact answer. Teams that skip this — relying on shared drives and file naming conventions like data_v3_final — cannot answer this question at all once enough time has passed and enough people have touched the folder.
Five things people get wrong about data version control
A zip file with a date in its name records that a version existed, but not what changed between versions, which code commit used it, or how to get back to it programmatically. DVC's pointer file ties a specific data hash to a specific Git commit automatically — checking out an old commit and running dvc checkout restores the exact matching data without anyone needing to remember which zip file went with which experiment. The zip folder approach also duplicates the full dataset for every version; DVC's content-addressable cache stores each unique chunk of data once, no matter how many versions reference it.
Both move large files out of the Git object database, but that is where the similarity ends. Git LFS still requires a Git remote that understands LFS pointers and has no concept of a data pipeline — it cannot express "rerun this stage only if this specific dependency changed." DVC adds pipeline stages (dvc.yaml), parameter tracking, metrics tracked across commits, and an experiment-comparison layer (dvc exp show) on top of the same content-addressable storage idea. Choosing Git LFS over DVC for an ML project usually means rebuilding all of that tooling yourself later, badly.
A backup answers whether you can recover this file if it is deleted. It does not answer which version of this file trained the model that is in production right now, because backups are not linked to the code commit or model artifact that consumed them. Versioning is a linkage problem, not a durability problem — you can have perfect nightly backups of every dataset that ever existed and still have no way to say which one a given model saw. DVC solves the linkage: the .dvc pointer committed alongside the training code is the permanent record of exactly which snapshot produced exactly which model.
DVC versions files, not queryable tables — it has no notion of point-in-time correctness, no SQL interface, and no low-latency lookups for online serving. A feature store (Module 69) solves a different problem: serving consistent, point-in-time-correct feature values to both training and real-time inference. In practice the two work together — the feature store produces a training snapshot, and DVC versions that snapshot as a file so it can be reproduced later. Neither tool replaces the other.
DVC guarantees that the same input files are available for a given commit — it does not guarantee the code that processes them is deterministic. A training script with an unseeded random split, a model that depends on library versions not pinned in requirements.txt, or a feature computation that calls an external API for live values will still produce different results on rerun even with byte-identical data. DVC removes one major source of irreproducibility; seeding randomness and pinning dependencies remove the others, and all three are required together.
DVC and data versioning — 5 questions interviewers actually ask
A model is a function of three things: the code that trained it, the hyperparameters, and the data. Git already versions the first two. Without versioning the data too, reproducing an experiment from six months ago is impossible even with the exact code and params checked out, because the underlying dataset has likely been overwritten, appended to, or regenerated since. Data versioning closes that gap: given a Git commit, you can recover the exact data that commit was trained and evaluated against, which is what makes a past result auditable, debuggable, and legally defensible in regulated domains like lending or healthcare.
Git LFS solves storage — it keeps large binary files out of the Git object database while still tracking them with Git-like commands. DVC solves storage the same way, using its own remote rather than a Git-LFS-aware host, but it also adds a pipeline layer on top: stages with explicit dependencies and outputs (dvc.yaml), a lockfile that lets dvc repro skip unchanged stages, tracked metrics and parameters comparable across commits, and a lightweight experiment-tracking mode (dvc exp) that runs variations without creating a Git commit for each one. Git LFS is a storage backend; DVC is a storage backend plus an ML workflow tool built on top of it.
Every model artifact should be tagged with the Git commit hash that produced it, ideally embedded directly in the model file or its accompanying metadata. From that commit, git show or git checkout on the .dvc pointer file reveals the MD5 hash and size of every dataset used to build it, and dvc pull retrieves the exact matching bytes from remote storage. In practice I would automate this into a model registry: every model version record stores its Git commit, and a single lookup — commit to .dvc hash to dvc pull — recovers the training data in a couple of commands, which is invaluable during an incident when someone needs to know exactly what a model saw.
DVC uses content-addressable storage: every file is stored in the cache under a path derived from its MD5 hash, not its filename. If a new dataset version changes 5 percent of rows and DVC tracks it as a single file, the whole file gets a new hash and is stored in full — DVC does not do byte-level diffing within a file. The real savings come from identical files being stored exactly once: if two branches or two experiments happen to produce byte-identical outputs, or if a file reverts to a previous exact state, DVC recognizes the matching hash and reuses the existing cached copy instead of storing it again.
First, stop it from getting worse — do not let that commit get merged or pushed further if it can still be avoided, since a large binary blob committed to Git history is expensive to remove later and bloats every future clone. If it has already been pushed, the fix is to run dvc add on the file to move it into DVC-managed storage, commit the resulting small .dvc pointer in place of the raw file, and then rewrite history to strip the large blob from earlier commits using something like git filter-repo, coordinating with the team since that rewrites shared history. Long term, I would add a pre-commit hook or CI check that rejects commits containing files above a size threshold that are not tracked by DVC.
Data is versioned. Next: design any ML system from first principles.
Module 75 is the final module of the MLOps section and one of the most practically valuable in the entire track — ML System Design. Given a real-world ML problem (build DoorDash's delivery time prediction system from scratch, or Stripe's fraud detection system), how do you design the full architecture? Data collection, feature engineering, model selection, serving infrastructure, monitoring, and the tradeoffs at each decision. This is what senior ML engineering interviews test and what every ML architect does on day one of a new project.
Design any ML system from scratch. The framework, tradeoffs, capacity estimation, and how to present it in an interview.
🎯 Key Takeaways
- ✓DVC adds data and model versioning on top of Git. It stores large files in S3/GCS and keeps tiny .dvc pointer files (200 bytes containing the MD5 hash) in Git. git checkout an old branch, then dvc checkout restores the exact data that branch used. Every model in your history has a corresponding dataset version, feature pipeline version, and code version.
- ✓Three storage locations work together: Git stores .dvc pointer files and dvc.yaml pipeline definitions (kilobytes), local .dvc/cache stores content-addressable data by MD5 hash (gigabytes), remote S3/GCS stores the shared team copy (same structure as local cache). dvc push uploads local cache to remote. dvc pull downloads from remote to local cache and workspace.
- ✓DVC pipelines (dvc.yaml) define stages with commands, deps (inputs that trigger reruns), outs (outputs tracked by DVC), params (hyperparameters from params.yaml), and metrics (small JSON files committed to Git). dvc repro only reruns stages where deps have changed — tracked in dvc.lock which must be committed to Git.
- ✓dvc exp run --set-param key=value runs an experiment with modified hyperparameters without creating a Git commit. dvc exp show compares all experiments in a table. dvc metrics diff HEAD~1 shows metric changes versus the previous commit. The best experiment is promoted with dvc exp apply then committed normally.
- ✓Never run dvc add on code files (.py, .yaml) — only on data files and model artifacts. Add *.py to .dvcignore to prevent accidental tracking. Always commit dvc.lock to Git — without it, DVC cannot detect what has changed and reruns everything. Commit metrics/ and plots/ files to Git (cache: false in dvc.yaml) so metrics are visible in git log and GitHub.
- ✓The complete team workflow: git pull && dvc pull (get latest), git checkout -b experiment/name (branch), edit code + params, dvc repro (run changed stages), dvc metrics diff main (compare to main), git add dvc.lock params.yaml metrics/ src/ && git commit, dvc push (upload data), git push. CI/CD runs dvc pull + dvc repro + metric assertions on every PR.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.