skip to content

What does a constraints file passed to pip with `-c` do that a requirements file with `-r` does not?

level: middleimportance: nice to knowfreq 20%

answer

  1. One file demands, the other only bounds
  2. It creates no work for the resolver
  3. Useful for a package you do not import
  4. Matching nothing is a valid outcome
  5. Partial bound, never a complete record

basics

~20 s

A constraints file only bounds versions: it says that if a distribution ends up being installed, it must satisfy this specifier. It never causes an installation. A requirements file passed with -r also decides what gets installed.

solid answer

~50 s

Both files hold requirement specifiers, but they play different roles in the resolution. Entries in a `-r` requirements file are **demands**: pip must install each of them. Entries in a `-c` constraints file are **bounds**: they restrict the version pip may choose for a distribution, but on their own they request nothing, so a constraint naming a package that never enters the graph is simply inert. That is exactly what makes it useful for transitive dependencies -- you can force a bad indirect release out of the resolution without adding a direct dependency you do not actually import. Constraints are also compositional: one shared file of organisation-wide floors can be passed alongside every service's own requirements. The limits follow from the semantics: constraint entries must be plain named requirements, editable entries are rejected, and because a constraint can silently match nothing, it is a bounding mechanism, never a record of a resolution.

go deeper

for a junior

Recall the distinction in one line: a requirements file installs things, a constraints file only limits which version is acceptable if something else asks for the package. Knowing the two flags apart is enough here.

for a middle

Explain the resolver semantics and the motivating case -- bounding a transitive dependency you do not import -- plus the consequence that an unmatched constraint is silent rather than an error.

for a senior

Show where it fits operationally: a shared floor file across services, why it is not a lock, and how you stop constraints accumulating into an invisible pile nobody dares remove.

for a principal

Decide whether centrally-published constraints are the right lever at all compared with an internal index, automated upgrade proposals, or per-service locks, and who owns the file once dozens of teams depend on it.

### The one-sentence difference `-r` says *install these*. `-c` says *and if you install this, it must be this version*. Everything else follows from that. ### Why the distinction matters The motivating case is a transitive dependency. Your service imports `frame-tools`, which in turn requires `clockdrift`. A `clockdrift` release goes out with a defect. You do not import `clockdrift`, so adding it to your own dependency list would be a lie: it announces a direct relationship that does not exist, and a future reader has no way to tell it was added only to dodge a bad release. Written as a constraint instead, the intent is legible -- *whatever pulls this in, do not let it be that version* -- and the entry becomes inert the day nothing needs the package any more. The second case is scale. A platform team can publish one constraints file expressing organisation-wide floors -- minimum versions that carry security fixes -- and every service passes it alongside its own requirements. Nobody has to enumerate which services happen to depend on what; the file only bites where the package is present. ### How pip consumes it ``` python -m pip install -r requirements.txt -c constraints.txt ``` Both flags may be repeated, and the constraint entries are merged into the resolution as extra bounds. A constraints file is a plain text file of requirement specifiers, so ranges, exact pins and environment markers all work: `clockdrift!=0.4.3`, `clockdrift>=0.4.4`, `clockdrift==0.4.2; python_version < "3.12"`. The restrictions come straight from the semantics. A constraint has to name a distribution -- an entry pip cannot associate with a name has nothing to bound. Editable entries are rejected, because installing a local project in place is a demand, not a bound. And a constraint that matches nothing produces no error, since nothing being installed is a perfectly valid outcome. ### Where teams misuse it **As a lockfile.** Copying pinned output into a constraints file and installing with only `-c` installs nothing at all -- there are no demands. Even paired with `-r`, the result is weaker than a lock: because unmatched constraints are silent, the file can rot for months while everyone believes it is pinning something. A lock is a *complete* record of a resolution; a constraints file is a *partial* bound on one. **As a substitute for fixing the direct dependency.** If a constraint is the only thing keeping a service on an old release of something it genuinely imports, that belongs in the project's own dependency ranges where a reviewer will see it. **Expecting it to force an upgrade.** A floor in a constraints file does not install the package or pull it in; it only rules out versions below the floor when something else asks for it. If the requirement is that a package be present at a minimum version, that is a requirement, not a constraint. ### The mental model to keep Think of the resolver as solving a set of equations. `-r` adds equations that must be satisfied. `-c` narrows the domain of a variable without asserting that the variable appears in the system. Both shape the answer; only one of them can create work.

  • Why can a constraints file not replace a lockfile even when every entry is pinned with `==`?
    Because constraints never demand an installation and never have to be complete. A pinned constraint on a package that no longer enters the graph is silently ignored, so the file can drift out of relevance with no signal, and it says nothing about the packages that *are* installed but absent from it. A lock records the entire resolved set; a constraints file bounds an arbitrary subset of it.
  • You need every service to move off a vulnerable release of an indirect dependency. Constraint or requirement?
    A constraint expressing the floor, published centrally and passed alongside each service's own requirements. It bites wherever the package is present, stays inert where it is not, and does not fabricate a direct dependency in projects that never import it. If a service must have the package installed regardless, that specific service adds a real requirement.

saying these in an interview costs you the question

  • Thinks a constraints file installs the packages it names
  • Uses a constraints file as a lockfile substitute
  • Expects a version floor in constraints to trigger an upgrade
  • Adds a fake direct dependency to pin a transitive one
  • Assumes an unmatched constraint raises an error

context