Dependencies and lockfiles
What a dependency actually is, how version ranges and lockfiles decide which code runs, how to read `requirements.txt`, `pyproject.toml`, `package.json` and `pnpm-lock.yaml`, and how to judge a package an agent added for you.
12 cards Β· 6 quiz questions Β· 10 min read
An agent adds one line to package.json and your dashboard suddenly has nine hundred new files in node_modules. Another agent adds pandas to a FastAPI service to parse one CSV column, and your Azure container image grows by 200 MB. Then a build that worked last week fails today with no code change at all. All three are the same subject: dependencies β other peopleβs code that runs inside your process β and the files that decide exactly which version of it you get.
A dependency is code you execute, not a service you call
When you install a package, the registry sends you source code. That code is then imported and run by your process, with your file system access, your environment variables, your database credentials. It is not sandboxed. pip install and npm install can also run install-time scripts on the machine doing the installing, which includes your CI runner.
That single fact explains most of what follows. A dependency is a decision to let a strangerβs code into your app, and every install is that decision repeated.
The count gets large fast, because of transitive dependencies β dependencies of your dependencies. You ask for FastAPI; you get Starlette, Pydantic, pydantic-core, typing-extensions, annotated-types and anyio. You can see the real shape of it:
uv tree # Python: the resolved dependency tree
uv pip list | wc -l # how many packages actually installed
pnpm why react-markdown # Node: who pulled this in, and why
pnpm list --depth 0 # your direct dependencies only
pnpm why is the one to remember. When an audit flags a package you have never heard of, it tells you which of your direct dependencies dragged it in β and therefore whether you can do anything about it.
Version ranges: what the symbols actually mean
Semantic versioning (semver) gives a release three numbers, MAJOR.MINOR.PATCH. MAJOR means the author broke something on purpose. MINOR means new features, nothing broken. PATCH means bug fixes only. Two caveats matter in practice: it is a convention kept by human discipline, not enforced by any tool, and anything below 1.0.0 is explicitly exempt, so a 0.x library can break you on a MINOR bump quite legitimately.
Manifests express ranges over that scheme:
{
"dependencies": {
"react": "^19.0.0", // >=19.0.0 <20.0.0 β minors arrive automatically
"react-markdown": "~9.0.1", // >=9.0.1 <9.1.0 β patches only
"zod": "3.23.8", // exactly this, nothing else
"some-lib": "^0.3.1" // >=0.3.1 <0.4.0 β caret is tight below 1.0
}
}
Python uses different punctuation for the same idea:
[project]
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115,<0.116", # allow patches, refuse the next minor
"pydantic>=2.7", # any 2.x from 2.7 up, including 3.0 β probably not what you want
"httpx~=0.27.0", # compatible release: >=0.27.0 <0.28.0
]
Note the middle line. pydantic>=2.7 with no upper bound will happily install Pydantic 3 the day it ships, and Pydantic 1-to-2 was a famously painful migration. Agents write unbounded lower-bound-only ranges constantly. They are fine in a library you publish and a hazard in an application you deploy.
The lockfile is what actually ran
A range is not a version. ^19.0.0 resolved to 19.0.0 in January and 19.2.4 in March, from the same source file. A lockfile removes that ambiguity: it records the exact version of every package in the entire tree, including transitive ones, along with the URL it came from and an integrity hash so tampering is detectable.
| Ecosystem | Manifest (what you asked for) | Lockfile (what you got) |
|---|---|---|
| npm | package.json | package-lock.json |
| pnpm | package.json | pnpm-lock.yaml |
| uv | pyproject.toml | uv.lock |
| Poetry | pyproject.toml | poetry.lock |
| pip-tools | requirements.in | compiled requirements.txt |
| bare pip | requirements.txt | none |
Both files belong in git. Then the install command has to be told to honour the lock, which is where most reproducibility bugs live:
# Agent's version β resolves ranges afresh, may rewrite the lockfile mid-build
npm install
pip install -r requirements.txt
# Better β install exactly the locked tree, fail loudly if lock and manifest disagree
npm ci
pnpm install --frozen-lockfile # already the CI default when a lockfile is present
uv sync --locked # errors rather than silently re-locking
uv has two similar flags worth separating: --locked verifies the lockfile is up to date with pyproject.toml and errors if not, while --frozen uses the lockfile as-is without checking. For a deploy you want --locked, so a dependency someone edited by hand and never re-locked fails the build instead of shipping.
The same applies in a Dockerfile, with a bonus: copying the lock files before the source code lets Docker cache the install layer.
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --locked --no-dev # no dev tooling in the image
COPY . .
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
--no-dev there is the other common production bug, in reverse. If pytest and ruff are in your image, you have shipped tooling and its transitive packages to production for no reason. If something your app genuinely imports is sitting in the dev group, the container starts and dies instantly with ModuleNotFoundError, which is a five-minute fix that looks like a catastrophe at 7 am.
Supply-chain risk, concretely
Supply-chain risk is the risk that comes from code you did not write and did not review. It shows up in four recognisable shapes.
Known vulnerabilities. A package you depend on has a reported flaw. Tooling finds these:
npm audit --omit=dev # your resolved tree vs the npm advisory database
npm audit fix # applies in-range upgrades only
pip-audit # current environment vs the Python advisory DB
pip-audit -r requirements.txt # or a specific requirements file
Read the output for two things: severity, and whether the flawed package is on a path you actually exercise. A prototype-pollution flaw in a build-time tool matters far less than an authentication bypass in something your API imports at request time. And a clean report means only that nothing in your tree matches a published, known advisory. It is a smoke alarm, not a structural survey.
Abandoned packages. No advisory will ever fire, because nobody is looking. Check the last publish date and open-issue count before depending on something.
Typosquatting and hallucinated names. Someone registers requsts, or python-dateutils, or a plausible scoped name, and waits. LLMs make this worse: a model will confidently import a package that does not exist, and attackers watch for those names and register them. So when a diff introduces a dependency you do not recognise, check it exists under exactly that name and owner:
npm view react-markdown name version time.modified dist.tarball
pip index versions pydantic # available versions from the index
curl -s https://pypi.org/pypi/httpx/json | python -c "import json,sys; d=json.load(sys.stdin); print(d['info']['home_page'])"
Compromised maintainer accounts. A legitimate package gets a malicious release. This is the case lockfiles help with most: pinned versions with hashes mean a new bad release cannot reach you until someone deliberately updates.
When an agent adds a package you did not need
This is the most common review decision you will actually make. The pattern to watch for is a dependency added to solve something small:
# Agent's version β a new dependency to parse one date string
from dateutil.parser import parse
submitted = parse(row["submitted_at"])
# Better β the standard library already does ISO 8601
from datetime import datetime
submitted = datetime.fromisoformat(row["submitted_at"]) # Python 3.11+ handles "Z" too
// Agent's version β lodash for one lookup in a dashboard component
import { get } from "lodash";
const label = get(response, "survey.wave.label", "unknown");
// Better β optional chaining and a default, no dependency
const label = response?.survey?.wave?.label ?? "unknown";
Neither dateutil nor lodash is a bad library. The point is that each one is a permanent addition to what you ship, update and audit, bought to save three lines. Sometimes that trade is right β nobody should hand-roll a JWT library or a date-timezone database. It is right much less often than agents assume.
A rough rule: reach for a dependency when the problem is genuinely hard and getting it wrong is dangerous (cryptography, timezones, HTTP, parsing untrusted input). Decline it when the problem is small and the language already has an answer.
What to check in your own code
git statusafter an agentβs work: didpackage-lock.json,pnpm-lock.yamloruv.lockchange? If a lock file changed and no manifest did, ask why.git diff package.json pyproject.tomlon every agent PR, and search the diff for each new package name. Is it imported anywhere?- Grep your CI workflow and Dockerfile for
npm installandpip install -r. Replace withnpm ci,pnpm install --frozen-lockfileoruv sync --locked. - Grep
pyproject.tomlfor>=with no upper bound on anything that matters, especially Pydantic, SQLAlchemy and FastAPI. - Confirm your production install excludes dev groups (
--no-dev,--omit=dev) and that nothing your app imports lives in a dev group. - Run
npm audit --omit=devandpip-auditonce now, on each service, and note what comes back. Do it again monthly rather than never. - For any package you do not recognise, run
npm view <name>or open its PyPI page: check last publish date, repository link and download count before you trust it. - Check the lockfiles are committed and not in
.gitignoreβ an agent scaffolding a project sometimes puts them there.
Your `package.json` says `"react-markdown": "^9.0.1"`. The build worked on Monday and fails on Friday with no code change. Which explanation is most likely?
Sources
- Tom Preston-Werner et al. β Semantic Versioning 2.0.0 website The MAJOR.MINOR.PATCH contract that every caret and tilde range depends on.
- npm docs β package-lock.json website What a lockfile records and why npm commits it: the exact tree, resolved URLs and integrity hashes.
- Python Packaging Authority β Writing your pyproject.toml website The modern, standard way to declare Python project metadata and dependencies.
- uv docs (Astral) β Locking and syncing website Backs the `uv.lock` behaviour and the difference between `--locked` and `--frozen`.