Flexible library dependencies, strict application dependencies

13 June 2026

Your approach to dependencies should be different depending on what kind of software you’re making and where it fits in the wider ecosystem.

If other people are integrating your code into their own system or application, e.g. you’re making a library, you should not make strict decisions about your dependency versions.

This means that if you’re using a language with a dependency-resolving package manager like pip, npm, or cargo, you should declare your dependency versions with as wide a range as you can support. If you don’t have a package manager, then you should accept your dependencies from the environment, or provide build options to specify where to find dependencies.

Why? Frequently, systems will only support one version of each software component, so your code will probably have to share dependencies with other components in the system:

  • many programming language environments, e.g. Python, support only a single version of each package being installed
  • there may be a flow of data or objects between different instances of the dependency, and if the data is incompatible, one version must be selected for the whole system
  • only one version may be valid due to external constraints; e.g. if one of your dependencies handles communication with a server, the integrator will have to use the version corresponding to the version of the server they have
  • even when multiple versions can exist, the integrator may want to de-duplicate dependencies because it
    • reduces disk usage
    • makes it easier for them to track what components are in their system and upgrade them

If you’re integrating your software yourself, by building it and sending it straight to users, or deploying it onto your own servers—in other words, you’re making an application or service—then the situation is different. There’s no longer anyone else in the picture, so you don’t need to be flexible. In fact, you probably value predictability, and want the same versions of dependencies to be used every time you build or run your code. In this case, you should be as strict as possible in specifying your dependencies.

If you’re using a language with a package manager, you should make use of its “locking” mechanism if it has one, such as Cargo.lock, package-lock.json, or requirements_freeze.txt, and distribute this lockfile with your code. If you’re not, you can write build code to download your dependencies, or include it alongside your source code, potentially using a “hermetic build system” like Bazel or Pants which are designed for this kind of use case.

In summary: if other people are integrating your code, be flexible about your dependencies; if you’re integrating it, be strict.

P.S. Strict dependency specifications still have a place for libraries: development and test environments. You don’t want different developers to get different behaviours depending on which dependency versions they have; you want to be intentional and have multiple fixed environments to ensure that your code really does work with your specified range of versions. (Deciding how much of your promised version range you should explicitly test is something of an art and depends on many factors.)

P.P.S. The distinctions between these types of software is not always well defined, and frequently different parties disagree about how software should be treated. For example, someone may make an open source application and develop it against a specific set of dependencies. However, then a Linux distribution wants to package the app, and does so using the dependency versions they already have in their distribution. Sometimes they will even patch the application’s code to accomplish this. The app developers get annoyed that their app isn’t being delivered how they intended it, and the distro maintainers get annoyed that they have to hack around the app’s very specific assumptions. You can see one side of this in Packagers don’t know best, or for a similar conflict, Restyling apps at scale.