Attributes
Understand Rust attributes, their syntax, and how to use derive, cfg, test, and other attributes to configure and document your code within modules and crates
What Attributes Are
Rust attributes are metadata annotations you attach to items like functions, structs, modules, or the entire crate. They look like #[attribute] or #![attribute] and instruct the compiler to do something beyond ordinary code execution: generate trait implementations, conditionally exclude code, mark a function as a test, or suppress warnings, among many other tasks. An attribute is not a runtime construct—it is a compile-time directive that shapes how the compiler treats the annotated item.
Not Like Comments:
Attributes are not just documentation; they can actively change the compilation output. A #[derive(Debug)] doesn't just document that a struct is debuggable—it generates the actual Debug implementation.
Why Attributes Exist
Rust deliberately avoids a separate preprocessor or external build‑script language for common tasks. Many responsibilities that C or C++ hand to #ifdef, #define, or CMake scripts are handled directly in Rust source by attributes. That means the compiler sees your intent without requiring a separate toolchain pass, which leads to better error messages, IDE integration, and a single source of truth.
Consider a library that needs different implementations on Unix and Windows. Instead of juggling #[cfg(unix)] blocks and hoping the build script keeps up, you write the condition right alongside the code. The compiler then includes only the branch that matches the target. This explicit, in-source approach eliminates the common class of bugs where a flag is incorrectly set in a build system and parts of the code silently go missing or get compiled for the wrong platform.
Attributes also solve the problem of repetitive boilerplate. Implementing Debug, Clone, or PartialEq for every struct by hand is tedious and error-prone. With #[derive(Debug)], the compiler generates the implementation for you, guaranteed to be correct for the shape of your data.
How Attributes Work
Attributes are consumed early in compilation, after the code is parsed into an abstract syntax tree. Different attributes trigger different compiler behaviors:
- Derive macros (
#[derive(...)]) generate trait implementations by expanding procedural macros. - Conditional compilation (
#[cfg(...)]) decides whether an item is included in the current compilation unit at all. - Lint attributes (
#[allow(...)],#[warn(...)],#[deny(...)]) adjust the linting engine. - Testing (
#[test]) registers a function with the test harness. - Documentation (
#[doc = "…"]) injects documentation strings into the final output. - Miscellaneous (
#[inline],#[must_use],#[non_exhaustive]) tweak optimization, warnings, and API stability.
Rust distinguishes between outer attributes and inner attributes. Outer attributes are placed just before the item they modify:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
Inner attributes are placed inside an item’s opening delimiter, before any other content, and apply to the enclosing item itself. They start with #!:
mod network {
#![cfg(target_os = "linux")]
// All items in this module exist only on Linux
pub fn connect() { /* ... */ }
}
The inner attribute #![cfg(target_os = "linux")] says: “this entire network module is only compiled on Linux.” Placing it as an outer attribute on the mod keyword would not work because mod itself is not a compilable item that can be conditionally removed—only the module’s contents make sense to gate. This example shows how inner attributes apply scope to all items within a module, a nuance that is especially important when organizing code across platforms.
Inner vs. Outer Misplacement:
Using #![attribute] before a function instead of #[attribute] will either be a syntax error or, worse, apply the attribute to the enclosing module instead of the function. Double-check the placement when you see unexpected behavior.
Commonly Used Attributes
You do not need to memorize every attribute the compiler recognizes—many are situational. A handful, though, appear in almost every Rust codebase and are closely tied to module structure.
Derive: Automated Trait Implementations
The derive attribute triggers the compiler to produce an implementation of a given trait for a type. It can only be used on struct and enum definitions (and, in limited form, on union).
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub name: String,
pub timeout: u64,
}
After this definition, Config automatically has Debug, Clone, and PartialEq implementations. The generated code is equivalent to what you would write by hand, but the compiler ensures it stays in sync with the fields.
The traits you list must be in scope. Standard library traits like Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, and Default are always available. For custom derive macros from third-party crates, you need to import them first. If you forget the import, the compiler error will tell you the trait is not found—add use some_crate::MyTrait; and it resolves.
Derive Does Not Check Trait Logic:
derive only ensures the implementation compiles. If you derive Copy on a type that contains a String, the compiler will reject it because String is not Copy. The attribute doesn't bypass Rust's normal safety rules—it just automates writing the boilerplate.
Cfg: Conditional Compilation
The cfg attribute decides whether an entire item or block of code is compiled, based on the target platform, feature flags, or other boolean predicates. It is the mechanism behind cross-platform modules, optional dependencies, and testing isolation.
pub mod audio {
#[cfg(target_os = "linux")]
pub fn play(file: &str) {
// ALSA-based implementation
}
#[cfg(target_os = "windows")]
pub fn play(file: &str) {
// Win32 audio playback
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
pub fn play(file: &str) {
panic!("audio playback not supported on this platform");
}
}
If you compile this code for Linux, only the first play function is included; the Windows and fallback versions simply do not exist in the compiled binary. This prevents platform-specific dependencies from leaking into builds where they are not needed, and the compiler checks each conditional branch independently. The not(any(...)) guard gives a clear compile error on unsupported platforms rather than a cryptic linker error from missing symbols.
cfg predicates can be combined with all(), any(), and not(). Common predicates include:
target_os—"linux","windows","macos", etc.target_arch—"x86_64","aarch64", etc.target_feature—"sse2","avx", etc.feature— feature flags defined inCargo.toml.test— enabled when compiling tests.debug_assertions— enabled for debug builds.
Checking active cfg:
Run rustc --print cfg to see the full set of cfg predicates automatically set for your target. This list includes platform details that you can use in your own #[cfg(...)] checks.
Test: Marking Test Functions and Modules
The #[test] attribute tells the test harness: “this function is a test.” When you run cargo test, Rust compiles a special version of your code that includes all functions marked with #[test], runs them, and reports results.
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn addition_works() {
assert_eq!(add(2, 2), 4);
}
#[test]
#[should_panic(expected = "attempt to add with overflow")]
fn overflow_detected() {
add(usize::MAX, 1);
}
}
The outer #[cfg(test)] on the tests module means the entire module is only compiled when testing. Inside, #[test] marks individual functions. The #[should_panic] attribute expects the test to panic, which is how you verify that error conditions are caught. Everything inside #[cfg(test)] mod tests can access private items in the parent module because child modules see their parent’s private items—so you can test internal logic without exposing it publicly.
Don't Forget the Cfg Test Gate:
Omitting #[cfg(test)] on a test module will cause your test code to be compiled into release binaries, increasing binary size and potentially exposing internal functions. Always wrap test modules in #[cfg(test)].
Lint Controls: allow, warn, deny, forbid
Lint attributes give you fine-grained control over the compiler’s warning system. They are especially useful in a module when you know a particular warning is expected but you do not want to silence it globally.
mod internal {
// This function exists for backward compatibility but is not used internally.
#[allow(dead_code)]
pub fn legacy_setup() {
// ...
}
// We never want to miss unreadable expressions, so upgrade the lint to an error.
#[deny(unused_must_use)]
pub fn fallible_operation() -> Result<u32, String> {
Ok(42)
}
}
allowsuppresses a lint so you get no warning.warnemits a warning (this is the default severity for many lints).denyturns the lint into a hard error.forbidacts likedenybut cannot be overridden byallowlater in the scope.
A common use case in library crates is #[allow(dead_code)] on public API items that are not called inside the library itself but are intended for downstream consumers. Without the attribute, the compiler would warn about “function xyz is never used,” which is misleading for a public function.
Attribute Syntax and Placement
Attributes follow a strict syntactic pattern so the compiler can parse them unambiguously. An outer attribute is the # character, followed by [, the attribute name with optional input, and ]. An inner attribute inserts an exclamation mark immediately after the #.
-
Outer attribute on an item:
#[derive(Clone)] struct Data { value: i32 } -
Inner attribute at the top of a module, crate, or block:
pub mod network { #![cfg(unix)] pub fn connect() { /* ... */ } }
Inner attributes must appear before any other statements or items inside the enclosing block. The canonical example at the crate level is #![crate_type = "lib"] in lib.rs, which tells rustc to build a library. You can also place #![allow(unused_variables)] at the top of a module to suppress warnings for the entire module without annotating each function.
Only One Inner Attr Position:
Inner attributes are legal only in certain positions: at the very beginning of a source file before any items (applying to the crate), or immediately inside a block { ... } before the first item. Putting an inner attribute after an fn declaration inside the same {} is a compile error.
Attributes in Module and Crate Organization
Attributes are not just for code generation—they shape how modules and crates interact. A few patterns illustrate how they integrate with Rust’s module system.
#[path] for custom file paths. By default, mod network; looks for network.rs or network/mod.rs. You can override this with #[path = "some_custom_path.rs"] on the mod declaration. This is rare in application code but occasionally useful when auto‑generating code or integrating with other build tools.
#[path = "os_specific/network_linux.rs"]
mod network;
#[macro_export] for public macros. When a macro defined in a module needs to be visible across crates, you annotate it with #[macro_export]. Without it, the macro is private to the module even if the module is pub. Combined with the module system, this lets you define macros in internal sub‑modules and re-export them at the crate root via pub use.
mod internal_macros {
#[macro_export]
macro_rules! my_helper {
() => { /* ... */ };
}
}
pub use internal_macros::my_helper;
Macro Use Override:
In older Rust code you may see #[macro_use] on mod declarations to import macros from external crates. This is deprecated; modern Rust allows you to use macros like any other item, e.g., use serde::Serialize; for a derive macro.
#[doc] for documentation visibility. The #[doc(hidden)] attribute hides an item from the generated documentation even though it remains publicly accessible. A library might use this to expose a method for macro expansion but keep it out of the public API docs. This is a tool for crafting clear module interfaces without sacrificing internal flexibility.
#![cfg_attr(test, ...)] for attribute gating. Sometimes you want an attribute only in test mode. The cfg_attr helper applies a different attribute based on a condition:
#![cfg_attr(test, allow(dead_code))]
This inner attribute at the crate root tells the compiler to allow dead code only when compiling tests, keeping release builds strict.
Summary
Attributes are the annotation layer that sits atop Rust’s item tree—functions, structs, modules, and crates. They do not introduce new runtime behavior; they shape the compile-time environment. The most important practical takeaway is that attributes connect the module system to the toolchain: #[cfg(test)] separates test code from production code, #[derive(Debug)] generates trait implementations across module boundaries without manual work, and #[cfg(...)] lets you maintain a single codebase that compiles selectively for different targets.
A useful mental model is to think of attributes as “compiler configuration that travels with the code.” Instead of a separate build script deciding which modules to include on Windows, the source file says it directly. That locality is what makes large Rust projects maintainable: when you read a module, you can see the conditions under which it exists without consulting a distant build system. Combined with Rust’s explicit pub visibility and the module tree, attributes complete the toolkit for building robust, cross-platform libraries and applications.