Read this as a study guide instead

Package management

Dependencies, versions, and dependency conflicts.

Requested packages 2 packages
Include a known conflict
pick one version of each library that fits every requirementlibssl1.01.12.02.43.0libjson1.22.02.53.0

Check your own machine

These commands only read information — they change nothing. Run the block for your OS, then use the table to read your own numbers against the ideas above.

Windows

What is installed, and at which version PowerShell
winget list | Select-Object -First 15

What you should see The first fifteen rows of installed applications, each with its name, id, and version, and a column noting whether an update is available.

Note Requires the Windows Package Manager (winget); the command queries installed applications without changing anything.

What a package depends on PowerShell
winget show Git.Git

What you should see The details winget records for the Git.Git package, including a Dependencies field listing what it needs to install.

Note Requires the Windows Package Manager (winget); the command displays package metadata only.

macOS

What is installed, and at which version zsh
brew list --versions | head

What you should see The first several installed Homebrew formulae, each with its version number, piped through head so only the top of the list appears.

Note Requires Homebrew to be installed; the command reads the list of installed formulae without changing anything.

What a package depends on zsh
brew deps --tree git

What you should see The dependency tree for git, showing its direct dependencies and, indented beneath them, their own dependencies.

Note Requires Homebrew to be installed; the command analyzes dependency relationships without installing anything.

Linux

What is installed, and at which version bash
apt list --installed | head

What you should see The first several installed packages, each shown with its name and version, piped through head so only the top of the list appears.

Note apt prints a harmless warning to stderr that it does not have a stable CLI interface; the installed-package listing itself is unaffected.

What a package depends on bash
apt-cache depends bash

What you should see The dependencies the bash package declares, one per line, including any alternative packages that can satisfy a given dependency.

Note apt-cache reads the package metadata and does not change the state of the system.

Where each idea shows up in your output
ConceptWindowsmacOSLinux
Installed packages and their versionswinget listbrew list --versionsapt list --installed
The dependencies a package declareswinget show (Dependencies field)brew deps --tree gitapt-cache depends bash

The same ideas, as prose

These are the exact fragments the model serves — also available as an ordered study guide.

Installing software without doing it by hand

Installing a program the hard way means finding the right file, downloading it, discovering it needs three other things you do not have, and going to find those too. A package manager is the tool that ends that errand: you name the software you want, and it installs that software together with everything the software needs to run, in a single command.

It manages this by pulling from a known source — a repository the manager already knows how to reach and is configured to trust — instead of leaving you to hunt for files across the web. You ask for one thing by name; the manager works out the full set that has to be present, fetches it, and puts each piece where it belongs.

Nearly every system you will touch ships with one: apt on Debian and Ubuntu, brew on macOS, winget on Windows. They differ in their command names and their details, but they all answer the same request — get me this software and whatever it depends on, and do not make me assemble it by hand.

What a package is

A package is more than the software inside it. It is the software bundled together with a description of itself — metadata the package manager can read before it installs anything, like a flat-pack furniture box: the item itself plus a printed sheet listing every part inside and the tools you must already have for it to go together.

Three fields in that metadata do most of the work. A name identifies the package so it can be requested and found. A version says which release this is, so the manager can tell one copy from another. And a declared list of dependencies names the other packages this one needs in order to run.

That last field is what makes hands-off installation possible. Because every package states what it requires, the manager never has to guess — it reads the list and goes to fetch what the list names. Each package effectively carries the instructions for its own assembly.

Software built on other software

one install pulls in the whole chain package you ask for library A library B library C depends on depends on depends on
A dependency graph: the package you request depends on two libraries, and one of those depends on a third, so the manager installs the whole transitive set, not only the package you named.

Almost no program stands on its own. It calls into libraries someone else wrote, which call into libraries someone else wrote, and so on down. A package captures this by declaring its direct dependencies: the packages it needs immediately in order to function.

The manager's job is to follow that declaration all the way down. A package you asked for depends on another, and that second package has its own dependencies, and those have theirs. So the manager installs not just what you named but the whole reachable set — the direct dependencies, their dependencies, and onward until nothing new is left to add. Following the chain through every level like this is what makes a dependency transitive.

The shape this produces is a graph: your package at the top, arrows to what it needs, more arrows out of each of those. One requested install can fan out into many packages you never named, which is exactly the bookkeeping a package manager exists to spare you.

Versions and compatibility

one version number, three fields 2 . 4 . 1 major minor patch breaking changes compatible additions compatible fixes a dependency asks for a range, such as at least 2.4 and below 3.0
The version number 2.4.1 splits into three fields: a major field (2) that changes on incompatible, breaking changes, a minor field (4) that changes on backward-compatible additions, and a patch field (1) that changes on backward-compatible bug fixes.

A version number is how a package says which release it is, and the common format packs three facts into one string: major.minor.patch, as in 2.4.1. This scheme is called semantic versioning, and each field changes for a different reason.

The rules are precise. The major field increases when a release makes incompatible changes — code written against the old version may break on the new one. The minor field increases when a release adds functionality but stays backward compatible, so existing code keeps working. The patch field increases for backward-compatible bug fixes, the safest kind of update. Read left to right, the fields run from "this might break you" to "this only helps you."

This is why a dependency is usually stated as a range rather than a single number. A package that needs a feature added in 2.4 but fears the breakage a 3.0 release might bring can ask for at least 2.4 and below 3.0, and accept any release in between. Versions are the language packages use to tell each other what they can safely work with.

Where packages come from

The packages a manager installs are not scattered loose across the internet; they live in a repository — a curated collection the manager already knows how to reach and is configured to trust. Asking to install something is really asking the manager to go to that repository and bring back a named package.

It rarely brings back just one. When you request a package, the manager looks up what that package requires, and what those requirements require, until it has the full set that must be present. Then it works out a combination of versions that satisfies every requirement at once — a set in which each package gets a version its dependents can accept.

Only then does it act: it downloads the chosen packages and installs them in an order that respects the dependencies, so nothing is installed before the things it needs are already in place. Look up, then solve, then fetch and install — the single command you typed hides all three steps.

When requirements collide

two demands one version cannot meet package X package Y shared library no version fits both needs >=2.0 needs <2.0 ×
Two requested packages depend on the same shared library but demand incompatible version ranges: one needs a version at least 2.0, the other needs a version below 2.0, so no single version satisfies both and the resolver reports a conflict.

Most of the time the manager quietly finds a set of versions that works. The hard case is when it cannot. Two packages you want can each depend on the same shared library while demanding incompatible versions of it: one insists on version 2.0 or newer, the other needs a version below 2.0. No single version of that library satisfies both at once, like two houseguests who each insist on a different, mutually exclusive setting of the same shared thermostat: whatever single setting you choose leaves one of them unhappy.

This is what makes dependency resolution genuinely difficult rather than mere list-following. The manager is solving a constraint problem: across every package in the set, pick one version of each that meets all the requirements simultaneously. When such a combination exists, it installs it. When the constraints are truly irreconcilable, no combination exists, and the manager's only honest move is to stop and report that it cannot satisfy them.

That dead end has a name — dependency hell. It is why resolvers are careful, sometimes slow, and occasionally defeated: the collision is not a flaw in the tool but a real contradiction in what you asked for.

Reproducible environments and trusting your dependencies

Two people installing "the same" software from ranges can still end up with different exact versions, because a range admits many. A lockfile closes that gap: it records the exact version of every package the resolver chose, so the next install rebuilds the identical set instead of re-solving and possibly drifting. It is how a project on your machine and the same project on a server become genuinely identical rather than merely similar.

Reproducibility is one reason to care; trust is the other. Every dependency you pull in is code you will run but did not write, so each install quietly extends your trust to everyone upstream of it — the supply-chain side of package management, and the reason who may publish to a repository is a security question and not only a convenience.

Both concerns compound as systems grow. Standing up a homelab or wiring together an agent pipeline means depending on layers of software you did not author and could not rebuild by hand; package management is the discipline that keeps that stack reproducible and its origins accountable. The question it keeps answering is the quiet one behind every install: exactly what am I running, and do I trust where it came from?