Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 041 / 100

Clippy and rustfmt: a practical guide for developers

Keep code clean automatically.

Expona Admin8 min read

Introduction

Code formatting and linting are often treated as nice-to-have luxuries, but they're actually foundational to shipping reliable Rust. We've seen teams waste countless hours in code review debates about spacing, or miss real bugs hiding in warnings we all learned to ignore. This guide covers Clippy and rustfmt: a practical guide for developers who want to eliminate these friction points and keep codebases clean automatically.

Rust's tooling ecosystem gives us two remarkable tools out of the box. rustfmt handles formatting consistently across the entire team, and Clippy catches logical errors, performance problems, and idiomatic violations before they reach production. When wired into CI properly, they become invisible guardrails that improve code quality without requiring a single code review comment about whitespace.

rustfmt — Consistent formatting without debate

rustfmt is Rust's official code formatter. Running cargo fmt rewrites your code to match a consistent style, and crucially, it's opinionated enough that there's almost nothing to debate. This alone saves time and mental energy across the team.

Most projects ship with rustfmt's defaults and never need to customize. That's intentional—the Rust team chose sensible defaults that work for the vast majority of codebases. In a microservices environment, we've standardized on rustfmt across all services without a single configuration file, and that consistency makes code reviews focus on logic, not tabs versus spaces.

To format your entire workspace:

rust
cargo fmt

To check formatting without modifying files (useful in CI):

rust
cargo fmt -- --check

For a real example, consider this slightly messy code:

rust
fn process_order(id: u64, items: Vec<Item>) -> Result<Order, OrderError> {
let   total = items.iter().map(|i| i.price).sum::<f64>();
    let discount = if total > 100.0 { 0.1 } else { 0.0 };
let final_price=total*(1.0-discount);
    Ok(Order { id, total: final_price, items })
}

After cargo fmt:

rust
fn process_order(id: u64, items: Vec<Item>) -> Result<Order, OrderError> {
    let total = items.iter().map(|i| i.price).sum::<f64>();
    let discount = if total > 100.0 { 0.1 } else { 0.0 };
    let final_price = total * (1.0 - discount);
    Ok(Order { id, total: final_price, items })
}

Spacing is consistent, indentation is correct, and operators have breathing room. Nobody on the team needs to think about this anymore. If you customize rustfmt at all, create a rustfmt.toml at the workspace root with minimal overrides:

rust
edition = "2021"
max_width = 100
hard_tabs = false
tab_spaces = 4

Most teams we work with never change these values. The defaults work well for production systems handling high throughput and complex domain logic.

Clippy — Catching real bugs and style issues

Clippy is a linter that catches common mistakes, performance anti-patterns, and idiomatic violations. It's more aggressive than rustc's own warnings, and it often prevents real bugs from reaching production. Running cargo clippy should be part of your standard development loop, right after cargo test.

Clippy comes with lint groups organized by severity: correctness, performance, complexity, style, and others. Most of these are enabled by default, but you can be stricter if you want:

rust
cargo clippy -- -D warnings

This treats all warnings (including Clippy's) as errors, which is exactly what you want in CI.

Here's a real scenario from an e-commerce service: a developer wrote this code to fetch products:

rust
pub fn get_product_prices(products: &[Product]) -> Vec<f64> {
    let mut prices = Vec::new();
    for p in products {
        prices.push(p.price);
    }
    prices
}

Running cargo clippy immediately flags this:

rust
warning: this loop could be written as a call to `map`
 --> src/lib.rs:2:5
  |
2 |     for p in products {
3 |         prices.push(p.price);
  |
  = help: ...or consider collecting into a `Vec` and using `extend`

warning: <generic> error: you are creating a `Vec` and immediately pushing into it

The idiomatic Rust version is cleaner and potentially more efficient:

rust
pub fn get_product_prices(products: &[Product]) -> Vec<f64> {
    products.iter().map(|p| p.price).collect()
}

Clippy catches dozens of patterns like this: unnecessary allocations, inefficient string operations, redundant closures, clone-and-drop patterns, and logic errors. In one CRM backend, Clippy found that we were cloning a database connection pool unnecessarily in every middleware layer—a performance win that code review alone might have missed.

Run Clippy regularly during development:

rust
cargo clippy --all-targets --all-features

The --all-targets flag checks tests and examples too, catching issues that might only surface in those contexts. --all-features ensures you're checking all code paths, which matters in libraries with multiple optional features.

CI integration — Fail builds on warnings

Formatting and linting are most powerful when they're enforced in CI, not left as suggestions. Many teams we've worked with saw a dramatic improvement in code quality the moment they started failing CI builds on warnings.

Here's a typical GitHub Actions workflow:

rust
name: Lint and Format
on: [push, pull_request]

jobs:
  fmt:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo fmt -- --check

  clippy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo clippy --all-targets --all-features -- -D warnings

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo test

The key line is cargo fmt -- --check, which returns a non-zero exit code if any file is misformatted. Similarly, -- -D warnings converts all warnings into hard errors. When a developer pushes code that doesn't pass these checks, CI blocks the merge until they run cargo fmt locally and push again.

This approach eliminates the most tedious part of code review. Nobody has to comment "please add a space before this brace" ever again. The machine handles it, and developers fix it in seconds.

In a trading system with strict latency requirements, we also added performance lints to catch allocations and unnecessary moves in hot paths:

rust
cargo clippy --all-targets --all-features -- \
  -D warnings \
  -D clippy::perf \
  -D clippy::correctness

This ensures that performance anti-patterns are caught before code review, letting humans focus on algorithmic correctness and business logic.

Configuring Clippy — Allow justified exceptions

Not every Clippy warning applies to every codebase. Sometimes you have valid reasons to deviate from a lint rule, and Rust gives you tools to document those exceptions explicitly.

The most common approach is to allow a specific lint at the module or function level using #[allow(...)]:

rust
#[allow(clippy::too_many_arguments)]
fn create_user(
    first_name: String,
    last_name: String,
    email: String,
    phone: String,
    address: String,
    city: String,
    country: String,
) -> User {
    // implementation
}

We added this #[allow(...)] because the business logic genuinely requires these seven parameters; we can't refactor them into a struct without breaking the API contract with other teams. The exception is documented right where it matters.

For more complex scenarios, create a .clippy.toml in your workspace root:

rust
too-many-arguments-threshold = 10
doc-markdown-code-line-threshold = 100

You can also define a custom lint profile using attributes. If your team has specific style preferences that Clippy doesn't enforce, you can create your own lint rules, though this is rare and typically only necessary for very large organizations with strict standards.

In a ride-hailing backend, we disabled the clippy::result_unit_err lint globally because our domain model uses Result<T, ()> for signal-based error handling in certain contexts:

rust
#![allow(clippy::result_unit_err)]

This goes at the top of lib.rs and applies to the entire crate. Always add a comment explaining why:

rust
// Our service uses Result<T, ()> for quick signal propagation in retry loops.
// The overhead of a full Error type isn't justified for these hot paths.
#![allow(clippy::result_unit_err)]

The key principle: exceptions should be explicit, justified, and documented. A developer reading the code two years later should understand why this lint was disabled.

Common mistakes

Ignoring Clippy suggestions in development. Some developers turn off warnings or just ignore them. This defeats the purpose. Clippy is your teammate; it's making genuine suggestions. Make it part of your regular workflow, like running tests.

Not checking formatting in CI. If cargo fmt -- --check isn't in your CI pipeline, you'll lose consistency as the team grows. One developer prefers four-space indents, another uses tabs; soon the codebase is a mess.

Allowing too many exceptions without justification. If you have dozens of #[allow(...)] attributes scattered throughout your code, you've essentially disabled the linter. Step back and ask: is this Clippy rule genuinely wrong for your domain, or are we just avoiding refactoring?

Using outdated Rust toolchain in CI. Clippy adds new lints regularly, and old versions might miss issues. Update your Rust toolchain in CI at least monthly. Using a pinned stable version (like dtolnay/rust-toolchain@stable) keeps you current without chasing nightlies.

Key takeaways

  • rustfmt removes formatting debates entirely. Run cargo fmt before every commit and cargo fmt -- --check in CI. Use defaults unless you have a specific reason not to.
  • Clippy catches real bugs and idiom violations. Make cargo clippy -- -D warnings part of your standard development loop and CI pipeline. It's not pedantry; it's practical error prevention.
  • Fail CI builds on formatting and lint warnings. This forces the team to maintain standards without code review friction. Developers fix issues in seconds locally before the build even reaches the repo.
  • Document exceptions explicitly. When you disable a lint with #[allow(...)], explain why. Future maintainers should understand the reasoning immediately.
  • Treat tooling as part of your development culture. Teams that embrace rustfmt and Clippy ship cleaner code, spend less time in review, and catch more bugs early. It's not extra work; it's work multiplication.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles