The Cargo Guide

Inspecting and Changing Dependencies

Why Dependency Inspection and Change Management Matter

Once a Cargo package has dependencies, the job is no longer just to declare them. You also need to inspect the resolved graph, change dependencies intentionally, understand what changed, and avoid unnecessary churn.

A useful mental model is:

  • declaring dependencies defines policy
  • resolving dependencies produces a concrete graph
  • inspecting dependencies shows what actually happened
  • changing dependencies safely means understanding both the manifest and the lockfile

The Main Activities in This Area

The core activities covered in this guide are:

  • adding dependencies
  • removing dependencies
  • updating dependencies
  • inspecting the dependency tree
  • understanding what changed in the graph
  • selectively updating one package
  • patching one crate for testing or temporary overrides
  • minimizing lockfile churn
  • tracing why a crate appears in the graph

These activities are connected. A dependency change is easiest to understand when you can inspect the graph before and after.

A Small Example Package

Suppose we start with a small package:

cargo new dep_manage_demo --lib
cd dep_manage_demo

Initial manifest:

[package]
name = "dep_manage_demo"
version = "0.1.0"
edition = "2024"
 
[dependencies]

Simple source:

pub fn greet(name: &str) -> String {
    format!("Hello, {name}!")
}

This package is small enough to understand clearly, but it can still demonstrate the full dependency management workflow.

Adding a Dependency by Editing Cargo.toml

The traditional way to add a dependency is to edit Cargo.toml directly.

For example:

[dependencies]
regex = "1"

Then use it in code:

use regex::Regex;
 
pub fn contains_number(s: &str) -> bool {
    Regex::new(r"\d").unwrap().is_match(s)
}

Then build:

cargo build

This works well and teaches the structure of the manifest directly.

Adding a Dependency with cargo add

Cargo also provides cargo add to add or modify dependencies from the command line.

Add a normal registry dependency:

cargo add regex

Add a specific requirement:

cargo add serde@1

Add a dev dependency:

cargo add --dev pretty_assertions

Add a build dependency:

cargo add --build cc

Add a path dependency:

cargo add --path ../core_utils core_utils

Add a git dependency:

cargo add --git https://github.com/example/parser.git parser

A strong practical model is:

  • direct Cargo.toml editing teaches manifest structure
  • cargo add is often faster and more uniform in day-to-day work

Adding Optional and Renamed Dependencies

You can also add more specialized dependencies.

Optional dependency:

cargo add serde --optional --features derive

This may produce something like:

[dependencies]
serde = { version = "1", optional = true, features = ["derive"] }

Renamed dependency:

cargo add serde_json --rename json_core

Which may produce:

[dependencies]
json_core = { version = "1", package = "serde_json" }

Then in Rust code:

pub fn parse_value() -> json_core::Value {
    json_core::from_str("{\"x\":1}").unwrap()
}

This helps keep dependency naming intentional rather than accidental.

Removing Dependencies

You can remove dependencies either by editing Cargo.toml manually or by using cargo remove.

For example:

cargo remove regex

Remove a dev dependency:

cargo remove --dev pretty_assertions

Remove a build dependency:

cargo remove --build cc

If you remove a dependency from the manifest, you should also remove or refactor any code that still uses it.

For example, after removing regex, code like this must change:

use regex::Regex;
 
pub fn contains_number(s: &str) -> bool {
    Regex::new(r"\d").unwrap().is_match(s)
}

Updating Dependencies

Dependency updates often happen through the lockfile rather than through manifest edits.

Update the whole lockfile to the newest allowed versions:

cargo update

Update one package selectively:

cargo update -p regex

A useful mental model is:

  • edit Cargo.toml when policy should change
  • run cargo update when you want the lockfile to move to newer versions already allowed by policy

When a Manifest Edit Is Needed vs When cargo update Is Enough

Suppose your manifest contains:

[dependencies]
regex = "1"

If a newer compatible 1.x version exists, you often do not need to change the manifest at all. You can usually update the lockfile:

cargo update -p regex

But if you want to move to a new major version line or otherwise change policy, then you need to edit the manifest:

[dependencies]
regex = "2"

Then build and test again:

cargo build
cargo test

This distinction is one of the most important ideas in dependency maintenance.

Inspecting the Dependency Tree

The main tool for inspecting the resolved graph is cargo tree.

cargo tree

This shows the current dependency tree as Cargo resolved it.

A useful model is:

  • Cargo.toml shows what you asked for
  • cargo tree shows what Cargo actually resolved

That makes cargo tree the first command to reach for when the graph feels bigger or stranger than expected.

Finding Duplicate Versions

To inspect duplicate crate versions in the graph, use:

cargo tree -d

This is especially useful when:

  • compile times feel unexpectedly large
  • binary size seems larger than expected
  • you suspect multiple versions of the same crate are being compiled

A duplicate is not automatically a bug, but it is often a useful signal that the graph deserves closer inspection.

Inspecting Feature Flow

To inspect how features are being enabled across the graph, use:

cargo tree -e features

This helps explain why a dependency ended up with more features enabled than you expected.

For example, you might think only one feature is active, but the graph may reveal that another dependency path requested additional features too.

Auditing Which Package Pulled in What

A very common question is:

"Why is this crate in my graph at all?"

To investigate that, use an inverted tree:

cargo tree -i some_crate

This shows which package paths lead into some_crate.

That is one of the most effective ways to audit which dependency pulled in a surprising transitive crate.

A Concrete Tree Inspection Example

Suppose your manifest says:

[dependencies]
regex = "1"
serde_json = "1"

And your code says:

use regex::Regex;
 
pub fn has_number(s: &str) -> bool {
    Regex::new(r"\d").unwrap().is_match(s)
}

You can inspect the resolved graph with:

cargo tree

Then inspect duplicates:

cargo tree -d

Then ask who pulled in a specific crate:

cargo tree -i serde_core

Even if you never named serde_core directly, this helps you locate the path by which it entered the graph.

Reasoning About Outdated Dependencies

In practice, "outdated" rarely means only "not the newest release." A dependency may be outdated in several senses:

  • the lockfile is behind the newest version already allowed by your manifest
  • the manifest requirement is pinned to an older policy than you want
  • the graph contains older duplicates because transitive dependencies are not aligned
  • a dependency is effectively stale because it remains in the graph even though your code no longer needs it

A good dependency review asks not just "is there something newer?" but also:

  • do we want to move?
  • would the move change policy or only lockfile state?
  • what graph churn would it cause?

Understanding What Changed in the Graph

When dependencies change, it is useful to inspect both the manifest and the resolved graph.

A practical workflow is:

git diff Cargo.toml Cargo.lock
cargo tree

This helps answer two separate questions:

  • what dependency intent changed?
  • what concrete graph changed?

Those are related, but they are not the same question.

A Before-and-After Workflow

Suppose you start with:

[dependencies]
regex = "1"

Then add another dependency:

cargo add serde_json

Now inspect changes:

git diff Cargo.toml Cargo.lock
cargo tree

This lets you see:

  • the manifest edit introducing serde_json
  • the lockfile changes for serde_json and its transitive graph
  • the actual resolved tree after the change

This is much more informative than only looking at the manifest.

Selective Updates to Minimize Churn

When you only want to move one package and minimize graph churn, selective updates are often better than a broad cargo update.

Example:

cargo update -p regex

This helps when:

  • a single dependency has a desired fix
  • you want a narrower change for review
  • you do not want to refresh the entire lockfile opportunistically

This is one of the easiest ways to keep dependency movement intentional.

Patching One Crate with [patch]

Cargo supports overriding a dependency source with [patch], which is especially useful for testing a fix or working with an unpublished dependency change.

Example using a local path override:

[dependencies]
uuid = "1"
 
[patch.crates-io]
uuid = { path = "../uuid" }

This tells Cargo:

  • normal manifest policy still says uuid comes from crates.io
  • but during resolution, use the local path version instead

This is very useful when testing a bugfix before it is published.

A Practical [patch] Example

Suppose you depend on a crate named text_core from crates.io:

[dependencies]
text_core = "1"

You discover a bug and fix it in a local checkout. You can patch it in:

[patch.crates-io]
text_core = { path = "../text_core" }

Then rebuild:

cargo build
cargo test

This lets you test your local fixed version without rewriting the ordinary dependency declaration.

When to Use [patch] vs When to Edit the Dependency Directly

A good mental rule is:

  • use ordinary dependency edits when the source policy itself should change
  • use [patch] when you want a temporary or local override layered on top of normal dependency intent

For example, this changes policy directly:

[dependencies]
text_core = { path = "../text_core" }

Whereas this keeps the normal registry dependency but overrides resolution locally:

[dependencies]
text_core = "1"
 
[patch.crates-io]
text_core = { path = "../text_core" }

That distinction matters a lot in collaborative workflows.

Minimizing Churn in Team Workflows

Dependency changes are easier to review when they are narrow and intentional.

A simple low-churn workflow looks like this:

cargo add serde_json
cargo build
cargo test
git diff Cargo.toml Cargo.lock

Or for an update:

cargo update -p regex
cargo test
git diff Cargo.lock

This reduces the chance that one small change quietly drags a large unrelated graph refresh along with it.

Using cargo metadata for Machine-Readable Inspection

For tooling and deeper automation, Cargo provides machine-readable metadata output.

cargo metadata --format-version 1

This is useful when:

  • you want to script dependency inspection
  • editor or CI tooling needs structured graph data
  • you want more than a terminal tree view

A practical mental model is:

  • cargo tree is human-facing graph inspection
  • cargo metadata is tool-facing graph inspection

Understanding Why the Graph Changed

When the graph changes unexpectedly, the main causes are usually one of:

  • you edited Cargo.toml
  • a lockfile update moved a package within an allowed range
  • a new dependency pulled in a larger transitive graph
  • feature activation changed the effective graph
  • a patch override changed the source or shape of the graph

That is why the best diagnosis flow usually combines:

git diff Cargo.toml Cargo.lock
cargo tree
cargo tree -d
cargo tree -e features

This lets you compare declared changes, lockfile changes, duplicate versions, and feature flow together.

Removing Dead Dependencies

Sometimes dependency maintenance is not about adding or updating. It is about removing a dependency that is no longer needed.

Suppose the manifest still contains:

[dependencies]
regex = "1"

But your code no longer uses it. Then you can remove it:

cargo remove regex
cargo build
cargo test

This helps keep compile times, graph size, and maintenance burden lower.

A Full Dependency Maintenance Session

Here is a realistic maintenance sequence for one package.

Add a dependency:

cargo add serde_json

Inspect the graph:

cargo tree

Update one crate selectively:

cargo update -p regex

Inspect duplicates:

cargo tree -d

Trace who pulled in a surprising crate:

cargo tree -i some_crate

Patch a local fix:

[patch.crates-io]
some_crate = { path = "../some_crate" }

Then verify:

cargo build
cargo test

This is the kind of session where Cargo stops feeling like a simple package manager and starts feeling like a graph management tool.

Common Beginner Mistakes

Mistake 1: assuming Cargo.toml alone tells the whole dependency story.

Mistake 2: using broad cargo update when a selective update would be clearer.

Mistake 3: removing a dependency from the manifest without removing code that still uses it.

Mistake 4: trying to diagnose graph surprises without using cargo tree.

Mistake 5: rewriting dependency sources directly when [patch] would be the cleaner temporary override.

Mistake 6: treating lockfile changes as meaningless noise instead of part of the dependency review.

Hands-On Exercise

Create a small package and practice the full inspection-and-change loop.

Start here:

cargo new dep_change_lab --lib
cd dep_change_lab

Add dependencies:

cargo add regex
cargo add serde_json
cargo add --dev pretty_assertions

Write some code:

use regex::Regex;
 
pub fn has_number(s: &str) -> bool {
    Regex::new(r"\d").unwrap().is_match(s)
}

Now inspect the graph:

cargo tree
cargo tree -d
cargo tree -e features

Then remove one dependency:

cargo remove serde_json

Then update another selectively:

cargo update -p regex

Finally inspect what changed:

git diff Cargo.toml Cargo.lock
cargo tree

This exercise makes the relationship between manifest edits, lockfile changes, and graph inspection concrete.

Mental Model Summary

A strong mental model for inspecting and changing dependencies in Cargo is:

  • adding and removing dependencies changes manifest policy
  • updating dependencies often changes the lockfile's concrete graph rather than the manifest policy itself
  • cargo tree shows what Cargo actually resolved
  • cargo tree -d helps find duplicate versions
  • cargo tree -i some_crate helps explain why a crate is present
  • [patch] is the right tool for temporary or local dependency overrides
  • selective updates and narrow edits reduce churn
  • reviewing both Cargo.toml and Cargo.lock is the best way to understand what changed

Once this model is stable, dependency maintenance becomes much easier to treat as controlled graph management rather than guesswork.