Skip to content
Learn Rust
Theme

Macros, attributes and comments

Syntax · Lesson 6

A name followed by !, such as println! or vec!, is a macro call: the compiler expands it into other code first. An ordinary Rust function has a fixed list of parameters, so this is how println! takes any number of arguments and checks its format string before the program runs. A line that starts with #[...] is an attribute, a note to the compiler about the item below it. Comments start with //, and /// turns a comment into documentation. cargo fmt and cargo clippy format and check the code.

Coming from Gofmt.Printf("%d %s", n, s) is println!("{} {}", n, s): {} replaces the verbs, and the compiler checks the string against the arguments. Struct tags like json:"name" become attributes such as #[serde(rename = "name")] (from the serde crate). The nearest thing to //go:build linux is #[cfg(target_os = "linux")], which applies to one item, not a whole file. cargo fmt is gofmt.

Coming from Kotlinprintln("$name is $age") is println!("{name} is {age}"), and "%5d".format(n) becomes {:5} inside the same macro. @Test is #[test], @Suppress("unused") is #[allow(unused)], and what a data class writes for you (toString, equals) comes from #[derive(Debug, PartialEq)].

Coming from Pythonprint(f"{name} is {age}") is println!("{name} is {age}"), and the specs after the colon are close to yours ({x:>5}, {x:05}). One difference: Rust's {x:.2} means two decimals, where Python's .2 alone means two significant digits. @dataclass is closest to #[derive(...)]; a decorator that wraps a function has no direct equivalent, because attributes act while the program is compiled, not when it runs.

Coming from TypeScriptA template literal ${name} is {name} inside println! or format!, but only a plain name goes inside the braces, not a + b. /** JSDoc */ becomes ///, and Prettier and ESLint are closest to cargo fmt and cargo clippy.

Coming from JavaSystem.out.printf("%5d%n", n) is println!("{:5}", n), and println! adds the newline for you. Annotations become attributes: @Test is #[test], @Deprecated is #[deprecated], @SuppressWarnings is #[allow(...)]. There is no @Override, and assert! is always on, unlike Java's assert.

Coming from C#Attributes are in the family you know: the Rust Reference says its #[...] syntax comes from C#. [Fact] is #[test], [Obsolete] is #[deprecated], #if DEBUG is #[cfg(debug_assertions)], and $"{n,5}" is {n:5} inside println!.

Coming from Cprintf("%5.2f %d\n", x, n) is println!("{:5.2} {}", x, n): there is no %d or %s, and the compiler checks the string against the arguments, so a mismatch is always an error. (GCC warns about printf mismatches under -Wall, but the C standard still calls them undefined behaviour.) #define macros substitute tokens; Rust's name! macros receive parsed syntax.

Coming from C++C++20's std::format("{:>5}", n) already uses {} placeholders like println! (though {:d} is legal there and an error in Rust). #define macros substitute tokens; Rust's name! macros receive parsed syntax. [[nodiscard]] and [[deprecated]] are #[must_use] and #[deprecated].

The quick comparison

Rustsrc/main.rsRun ↗
//! Prints a few values in different formats.
/// A point on a grid. `derive(Debug)` lets `{:?}` print it.
#[derive(Debug)]
#[allow(dead_code)] // the fields are only printed, never read
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 3, y: 4 }; // a line comment
let items = vec![1, 2, 3];
assert_eq!(items.len(), 3); // stops with a message if false
println!("point: {p:?}");
println!("items: {items:?}");
println!("row: [{:>5}] [{:<5}] [{:05}]", 42, 42, 42);
let pi = format!("{:.2}", 3.14159);
println!("pi: {pi} {{done}}");
}
$ cargo run
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
package main
import (
"fmt"
"strings"
)
// Point is a point on a grid.
type Point struct{ x, y int }
func main() {
p := Point{3, 4} // a line comment
items := []int{1, 2, 3}
if len(items) != 3 {
panic("items should have 3 elements")
}
fmt.Printf("point: Point { x: %d, y: %d }\n", p.x, p.y)
fmt.Printf("items: %s\n", strings.ReplaceAll(fmt.Sprint(items), " ", ", "))
fmt.Printf("row: [%5d] [%-5d] [%05d]\n", 42, 42, 42)
pi := fmt.Sprintf("%.2f", 3.14159)
fmt.Printf("pi: %s {done}\n", pi)
}
$ go run main.go
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
/** A point on a grid. */
data class Point(val x: Int, val y: Int)
fun main() {
val p = Point(3, 4) // a line comment
val items = listOf(1, 2, 3)
check(items.size == 3) // stops with an exception if false
println("point: Point { x: ${p.x}, y: ${p.y} }")
println("items: $items")
println("row: [${"%5d".format(42)}] [${"%-5d".format(42)}] [${"%05d".format(42)}]")
val pi = "%.2f".format(3.14159)
println("pi: $pi {done}")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
from dataclasses import dataclass
@dataclass
class Point:
"""A point on a grid."""
x: int
y: int
p = Point(3, 4) # a line comment
items = [1, 2, 3]
assert len(items) == 3 # stops with an error if false
print(f"point: Point {{ x: {p.x}, y: {p.y} }}")
print(f"items: {items}")
print(f"row: [{42:>5}] [{42:<5}] [{42:05}]")
pi = f"{3.14159:.2f}"
print(f"pi: {pi} {{done}}")
$ python3 main.py
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
/** A point on a grid. */
interface Point {
x: number;
y: number;
}
const p: Point = { x: 3, y: 4 }; // a line comment
const items = [1, 2, 3];
if (items.length !== 3) throw new Error("items should have 3 elements");
console.log(`point: Point { x: ${p.x}, y: ${p.y} }`);
console.log(`items: [${items.join(", ")}]`);
console.log(`row: [${String(42).padStart(5)}] [${String(42).padEnd(5)}] [${String(42).padStart(5, "0")}]`);
const pi = (3.14159).toFixed(2);
console.log(`pi: ${pi} {done}`);
$ tsc --strict --module nodenext --types node main.ts && node main.js
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
import java.util.List;
public class Main {
/** A point on a grid. */
record Point(int x, int y) {}
public static void main(String[] args) {
Point p = new Point(3, 4); // a line comment
List<Integer> items = List.of(1, 2, 3);
if (items.size() != 3) throw new IllegalStateException("items should have 3 elements");
System.out.println("point: Point { x: " + p.x() + ", y: " + p.y() + " }");
System.out.println("items: " + items);
System.out.println(String.format("row: [%5d] [%-5d] [%05d]", 42, 42, 42));
String pi = String.format("%.2f", 3.14159);
System.out.println("pi: " + pi + " {done}");
}
}
$ javac Main.java && java Main
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
var p = new Point(3, 4); // a line comment
var items = new List<int> { 1, 2, 3 };
if (items.Count != 3) throw new Exception("items should have 3 elements");
Console.WriteLine($"point: Point {{ x: {p.X}, y: {p.Y} }}");
Console.WriteLine($"items: [{string.Join(", ", items)}]");
Console.WriteLine($"row: [{42,5}] [{42,-5}] [{42:D5}]");
var pi = 3.14159.ToString("F2");
Console.WriteLine($"pi: {pi} {{done}}");
/// <summary>A point on a grid.</summary>
record Point(int X, int Y);
$ dotnet run
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
#include <stdio.h>
/* A point on a grid. */
struct Point {
int x;
int y;
};
int main(void) {
struct Point p = {3, 4}; // a line comment
int items[] = {1, 2, 3};
if (sizeof items / sizeof items[0] != 3) return 1;
printf("point: Point { x: %d, y: %d }\n", p.x, p.y);
printf("items: [%d, %d, %d]\n", items[0], items[1], items[2]);
printf("row: [%5d] [%-5d] [%05d]\n", 42, 42, 42);
char pi[16];
snprintf(pi, sizeof pi, "%.2f", 3.14159);
printf("pi: %s {done}\n", pi);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}
#include <format>
#include <iostream>
#include <string>
#include <vector>
// A point on a grid.
struct Point {
int x;
int y;
};
int main() {
Point p{3, 4}; // a line comment
std::vector<int> items{1, 2, 3};
if (items.size() != 3) return 1;
std::cout << std::format("point: Point {{ x: {}, y: {} }}\n", p.x, p.y);
std::cout << std::format("items: [{}, {}, {}]\n", items[0], items[1], items[2]);
std::cout << std::format("row: [{:>5}] [{:<5}] [{:05}]\n", 42, 42, 42);
std::string pi = std::format("{:.2f}", 3.14159);
std::cout << std::format("pi: {} {{done}}\n", pi);
return 0;
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
point: Point { x: 3, y: 4 }
items: [1, 2, 3]
row: [   42] [42   ] [00042]
pi: 3.14 {done}

What's different

  • name!(...) is a macro call, not a function call. The compiler expands it before it checks types, which is how println! accepts any number of arguments although Rust functions cannot.
  • In a format string, {} prints with Display, {:?} with Debug, and a spec after the colon sets width, alignment and precision. {{ and }} print one brace each.
  • #[...] applies to the item after it, and #![...] to the file or module it sits in. The common ones are derive, test, allow and cfg.
  • // and /* */ are comments (block comments nest). /// documents the item below it and //! the file around it; cargo doc builds web pages from them.
  • cargo fmt rewrites your code in the standard style, and cargo clippy adds more checks than the compiler runs.
Gotcha

Using %d in a format string

Looks fine: in Go, Kotlin, Python, TypeScript, Java, C and C++ it runs fine; in C# it runs but gives a wrong answer. Rust won't compile it.

Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Broken in C#. Rust catches it.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
fn main() {
let count = 5;
println!("count: %d", count);
}
package main
import "fmt"
func main() {
count := 5
fmt.Printf("count: %d\n", count)
}
$ go run main.go✓ runs
count: 5
fun main() {
val count = 5
println("count: %d".format(count))
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
count: 5
count = 5
print("count: %d" % count)
$ python3 main.py✓ runs
count: 5
const count = 5;
console.log("count: %d", count);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
count: 5
public class Main {
public static void main(String[] args) {
int count = 5;
System.out.printf("count: %d%n", count);
}
}
$ javac Main.java && java Main✓ runs
count: 5
int count = 5;
Console.WriteLine("count: %d", count);
$ dotnet run! runs · wrong answer
count: %d
#include <stdio.h>
int main(void) {
int count = 5;
printf("count: %d\n", count);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
count: 5
#include <cstdio>
int main() {
int count = 5;
std::printf("count: %d\n", count);
return 0;
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
count: 5
What does the compiler say? Guess first, then open.
$ cargo build
error: argument never used
 --> src/main.rs:3:27
  |
3 |     println!("count: %d", count);
  |                           ^^^^^ argument never used
  |
  = note: printf formatting is not supported; see the documentation for `std::fmt`
help: format specifiers use curly braces
  |
3 -     println!("count: %d", count);
3 +     println!("count: {}", count);
What happens in Go

Go prints count: 5. Printf takes verbs such as %d.

What happens in Kotlin

Kotlin prints count: 5. "count: %d".format(count) uses Java's format syntax.

What happens in Python

Python prints count: 5. The % operator on a string is Python's older formatting style.

What happens in TypeScript

TypeScript prints count: 5. Node's console.log treats %d in the first argument as a placeholder.

What happens in Java

Java prints count: 5. printf takes %d, and %n is the newline.

What happens in C#

C# compiles this and prints count: %d. Its composite format strings use {0}, so %d is plain text and the extra argument is ignored, with no compiler message by default.

What happens in C

C prints count: 5. printf is where %d comes from.

What happens in C++

C++ prints count: 5 with printf. std::format (C++20) uses {} as Rust does, though it accepts {:d}, which Rust does not.

What Rust sees

println! uses braces for placeholders. It sees %d as plain text, finds an argument with no placeholder, and stops with argument never used. It even points at the fix: {}.

Why Rust says no

Rust's format strings use {}, and the compiler checks that every argument has a placeholder and every placeholder has an argument. It does not know that %d means anything: %d is plain text to it, so an argument aimed at a %d is left without a placeholder and is rejected. (A %d with no argument at all compiles and prints %d.) The fix is {} with no letter: the argument's type decides how it prints, and {:?} asks for the debugging form. Most other languages run this program; the C# outputs show a version that runs but prints the wrong thing.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let count = 5;
println!("count: {}", count);
}
$ cargo run
count: 5

The toolkit

Three things in Rust code look like noise until someone names them: a ! after a name (println!), a line that starts with #[ (#[derive(Debug)]), and a comment that starts with ///. This page names each one and maps it from your language. It does not teach what the features are for (traits, tests, modules): it says “a later lesson” or links to the lesson that does.

The !: a macro call

An ordinary Rust function has a fixed list of parameters: there is no *args, no ...rest, no printf-style .... Yet println! takes any number of arguments, and checks its format string against them before the program runs. It can because it is a macro: code that the compiler expands into other code before it checks types (the Book). The ! is the marker; in C nothing at the call tells you a macro is involved, apart from the habit of capital letters. You will use macros constantly and write them rarely.

Macro What it does
print! println! eprintln! Print to standard output, with or without a newline; eprintln! to standard error.
format! Build a String instead of printing.
vec! Build a Vec, a growable list (Vectors).
assert! assert_eq! Panic (stop the program with a message) if a condition is false, or two values differ.
matches! true if a value fits a pattern (Match).
panic! todo! Panic now. todo! marks code you have not written yet, as the exercises below do.
dbg! Print the source location, the expression and its value to standard error, and give the value back.
write! Like format!, but into something you pass in, such as a file.
Rustsrc/main.rsRun ↗
fn main() {
print!("print! has no newline; ");
println!("println! adds one");
let s = format!("{}-{}", 1, 2);
println!("format! returns a String: {s}");
let v = vec![10, 20, 30];
println!("vec! builds a Vec: {v:?}");
assert!(v.len() == 3);
assert_eq!(v[0], 10);
let c = 'k';
println!(
"matches! is a yes/no pattern test: {}",
matches!(c, 'a'..='z' | 'A'..='Z')
);
let doubled = dbg!(v[1] * 2);
println!("dbg! returned the value: {doubled}");
}
$ cargo run
print! has no newline; println! adds one
format! returns a String: 1-2
vec! builds a Vec: [10, 20, 30]
matches! is a yes/no pattern test: true
[src/main.rs:15:19] v[1] * 2 = 40
dbg! returned the value: 40

A failed assert_eq! shows both sides. The third argument onward is a message, printed first:

Rustsrc/main.rs! panics at runtimeRun ↗
fn main() {
let total = 2 + 2;
assert_eq!(total, 5, "total was wrong after {} steps", 2);
}
$ cargo run
thread 'main' (1) panicked at src/main.rs:3:5:
assertion `left == right` failed: total was wrong after 2 steps
  left: 4
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Format strings

println!, format!, write!, panic! and the message of assert! take a format string: text where {...} marks a place for a value. What follows a colon inside the braces says how to print it.

You write It means
{} The next argument, in its Display form: the one meant for people.
{:?} and {:#?} The Debug form: the one meant for programmers. # spreads it over several lines.
{0}, {name} An argument by position or by name. {name} alone uses the variable name in scope.
{:5} {:<5} {:^5} {:>5} A width of 5, then left, centre and right alignment. {:*^9} picks the fill character.
{:05} {:+} {:.2} {:8.3} Zero padding. Always show the sign. Digits after the decimal point. Width, then precision.
{:x} {:#x} {:b} Hexadecimal, hexadecimal with 0x, binary. Flags combine: {:#010b} is the 0b prefix, zero fill and width 10.
{:w$} {:.p$} Width and precision taken from variables w and p.
{{ and }} One brace each.
Rustsrc/main.rsRun ↗
fn main() {
let name = "Ada";
let n = 255;
let pi = 3.14159265;
println!("escape: {{ and }}");
println!("positional: {0}-{1}-{0}", "a", "b");
println!("named: {who} is {age}", who = name, age = 36);
println!("inline: {name} {n}");
println!("display vs debug: {} {:?}", name, name);
println!(
"width: [{:8}] [{:<8}] [{:^8}] [{:>8}]",
name, name, name, name
);
println!("fill: [{:*^9}] [{:->9}]", name, name);
println!("integers: [{:5}] [{:<5}] [{:05}] [{:+}]", n, n, n, n);
println!("floats: [{}] [{:.2}] [{:8.3}]", pi, pi, pi);
println!("bases: {n:x} {n:X} {n:#x} {n:b} {n:#010b} {n:o}");
let (w, p) = (7, 1);
println!(
"from variables: [{:>w$}] [{:.p$}] [{:>w$.p$}]",
name, pi, pi
);
println!("too long: [{:3}]", "Lovelace");
println!("truncated: [{:.3}]", "Lovelace");
println!("one character: [{:>4}]", "é");
}
$ cargo run
escape: { and }
positional: a-b-a
named: Ada is 36
inline: Ada 255
display vs debug: Ada "Ada"
width: [Ada     ] [Ada     ] [  Ada   ] [     Ada]
fill: [***Ada***] [------Ada]
integers: [  255] [255  ] [00255] [+255]
floats: [3.14159265] [3.14] [   3.142]
bases: ff FF 0xff 11111111 0b11111111 377
from variables: [    Ada] [3.1] [    3.1]
too long: [Lovelace]
truncated: [Lov]
one character: [   é]

Text is left-aligned by default and numbers right-aligned. A value wider than its width is not cut off. Width counts chars (Unicode scalar values), not bytes or screen columns, and on text .3 is a maximum length. The std::fmt docs list the whole grammar.

Display and Debug are traits: named abilities a type can have (Traits). Text has both; your own struct has neither until you add it:

Rustsrc/main.rs✗ won't compile
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("point: {}", p);
}
$ cargo buildWhat is E0277?
error[E0277]: `Point` doesn't implement `std::fmt::Display`
 --> src/main.rs:8:27
  |
8 |     println!("point: {}", p);
  |                      --   ^ `Point` cannot be formatted with the default formatter
  |                      |
  |                      required by this formatting parameter
  |
help: the trait `std::fmt::Display` is not implemented for `Point`
 --> src/main.rs:1:1
  |
1 | struct Point {
  | ^^^^^^^^^^^^
  = note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead

The note offers {:?}, but a struct needs Debug first, which takes one line (#[derive(Debug)], next section):

Rustsrc/main.rs✗ won't compile
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("point: {:?}", p);
}
$ cargo buildWhat is E0277?
error[E0277]: `Point` doesn't implement `Debug`
 --> src/main.rs:8:29
  |
8 |     println!("point: {:?}", p);
  |                      ----   ^ `Point` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  |                      |
  |                      required by this formatting parameter
  |
  = help: the trait `Debug` is not implemented for `Point`
help: consider annotating `Point` with `#[derive(Debug)]`
  |
1 + #[derive(Debug)]
2 | struct Point {

The format letters of other languages do not exist; the type decides how a value prints:

Rustsrc/main.rs✗ won't compile
fn main() {
println!("{:d}", 5);
println!("{:.2f}", 3.14159);
}
$ cargo build
error: unknown format trait `d`
 --> src/main.rs:2:17
  |
2 |     println!("{:d}", 5);
  |                 ^
  |
  = note: the only appropriate formatting traits are:
          - ``, which uses the `Display` trait
          - `?`, which uses the `Debug` trait
          - `e`, which uses the `LowerExp` trait
          - `E`, which uses the `UpperExp` trait
          - `o`, which uses the `Octal` trait
          - `p`, which uses the `Pointer` trait
          - `b`, which uses the `Binary` trait
          - `x`, which uses the `LowerHex` trait
          - `X`, which uses the `UpperHex` trait

error: unknown format trait `f`
 --> src/main.rs:3:19
  |
3 |     println!("{:.2f}", 3.14159);
  |                   ^
  |
  = note: the only appropriate formatting traits are:
          - ``, which uses the `Display` trait
          - `?`, which uses the `Debug` trait
          - `e`, which uses the `LowerExp` trait
          - `E`, which uses the `UpperExp` trait
          - `o`, which uses the `Octal` trait
          - `p`, which uses the `Pointer` trait
          - `b`, which uses the `Binary` trait
          - `x`, which uses the `LowerHex` trait
          - `X`, which uses the `UpperHex` trait

Slips from other languages are compile errors too. Each of these four lines is one:

Rustsrc/main.rs✗ won't compile
struct Point {
x: i32,
}
fn main() {
let count = 5;
let p = Point { x: 1 };
println!("count:", count);
println!(count);
println!("x is {p.x}");
println!("count: " + count);
}
$ cargo build
error: argument never used
 --> src/main.rs:8:24
  |
8 |     println!("count:", count);
  |              --------  ^^^^^ argument never used
  |              |
  |              formatting specifier missing
  |
help: format specifiers use curly braces, consider adding a format specifier
  |
8 |     println!("count:{}", count);
  |                     ++

error: format argument must be a string literal
 --> src/main.rs:9:14
  |
9 |     println!(count);
  |              ^^^^^
  |
help: you might be missing a string literal to format with
  |
9 |     println!("{}", count);
  |              +++++

error: invalid format string: field access isn't supported
  --> src/main.rs:10:21
   |
10 |     println!("x is {p.x}");
   |                     ^^^ not supported in format string
   |
   = note: consider moving this expression to a local variable and then using the local here instead
help: consider using a positional formatting argument instead
   |
10 -     println!("x is {p.x}");
10 +     println!("x is {0}", p.x);

error: expected `,`, found `+`
  --> src/main.rs:11:24
   |
11 |     println!("count: " + count);
   |                        ^ expected `,`

The format string has to be a literal so the compiler can check it. So a log(format, args...) wrapper cannot forward them to println!: take a finished String (callers write log(&format!(...))) or write a macro. Forwarding with format_args! and fmt::Arguments is covered in Printing and Formatting.

Attributes: #[...]

An attribute is a note to the compiler about the thing after it: a function, a struct, a field, a statement. With a !, as in #![...], the note is about the thing it sits inside, such as the file or module.

Attribute What it does
#[derive(Debug, Clone, PartialEq)] On a struct or enum: the compiler writes the standard code for those traits. Debug is what {:?} needs, Clone gives you .clone(), PartialEq is what == needs.
#[test] Marks a test function. It is built and run by cargo test, and left out of a normal build.
#[cfg(...)] Keep the item only when a condition holds, such as target_os = "linux" or test; not(test) is the opposite.
#[allow(...)] #[warn(...)] #[deny(...)] Set what a lint does. See below.
#[deprecated] #[must_use] Callers get a warning if they use the item, or ignore its result.
#[tokio::main] #[serde(...)] Attributes that crates (libraries) define. See Macros you’ll read.
Rustsrc/main.rsRun ↗
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = a.clone();
println!("{{:?}} -> {:?}", a);
println!("{{:#?}} -> {:#?}", a);
println!("equal: {}", a == b);
}
$ cargo run
{:?}  -> Point { x: 1, y: 2 }
{:#?} -> Point {
    x: 1,
    y: 2,
}
equal: true

A derive works on the type directly below it, every field must have the trait too, and attributes stack. Clone is an explicit copy of the whole value. (Partial in PartialEq is explained in Common traits.)

Rustsrc/main.rsRun ↗
#[cfg(target_os = "linux")]
fn os() -> &'static str {
"linux"
}
#[cfg(not(target_os = "linux"))]
fn os() -> &'static str {
"something else"
}
fn main() {
println!("os: {}", os());
println!("debug build: {}", cfg!(debug_assertions));
}
$ cargo run
os: linux
debug build: true

That is the page’s runner (Linux, a debug build). cfg!(debug_assertions) is true when the build has no optimisation (Reference), as in a plain cargo build.

Lints

A lint is a check on style or on likely mistakes. Each has a level, usually allow (silent), warn (a message, the build continues) or deny (an error). An attribute changes the level for the thing below it, and #![deny(...)] for the whole file. The naming checks in Declarations are lints. The page shows compiler messages only for failures, so this program runs plain rustc on a three-line file three times (as it is, with #[allow], with #![deny]) and prints the relevant lines. You would see them with cargo build.

Rustsrc/main.rsRun ↗
use std::fs;
use std::process::Command;
const BODY: &str = "fn main() {\n let count = 3;\n}\n";
// Compile `source` with the plain `rustc` command and return what it printed.
fn rustc(source: &str) -> String {
let dir = std::env::temp_dir().join("lint-levels-demo");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("demo.rs"), source).unwrap();
let out = Command::new("rustc")
.args(["--edition", "2024", "demo.rs"])
.current_dir(&dir)
.output()
.unwrap();
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn main() {
println!("--- no attribute (the default level) ---");
print!("{}", rustc(BODY));
println!("--- #[allow(unused_variables)] ---");
let out = rustc(&format!("#[allow(unused_variables)]\n{BODY}"));
println!(
"{}",
if out.is_empty() {
"(nothing printed)"
} else {
&out
}
);
println!("--- #![deny(unused_variables)] ---");
let out = rustc(&format!("#![deny(unused_variables)]\n{BODY}"));
println!("{}", out.lines().next().unwrap());
}
$ cargo run
--- no attribute (the default level) ---
warning: unused variable: `count`
 --> demo.rs:2:9
  |
2 |     let count = 3;
  |         ^^^^^ help: if this is intentional, prefix it with an underscore: `_count`
  |
  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: 1 warning emitted

--- #[allow(unused_variables)] ---
(nothing printed)
--- #![deny(unused_variables)] ---
error: unused variable: `count`

The note under the warning says on by default: that lint warns without any attribute.

Comments and documentation

Form What it is
// text A comment to the end of the line. A plain // above a function is only a comment, not documentation.
/* text */ A block comment. Block comments nest.
/// text Documents the item below it, in Markdown. (/** text */ works too.)
//! text Documents the file or module around it. It is usually the first lines of a file.
Rustsrc/main.rsRun ↗
//! Inner doc comment: describes the file (crate) it sits in.
/// Outer doc comment: documents the item below it.
fn add(a: i32, b: i32) -> i32 {
// A line comment runs to the end of the line.
/* A block comment /* can nest */ and still be one comment. */
a + b
}
fn main() {
println!("2 + 3 = {}", add(2, 3));
}
$ cargo run
2 + 3 = 5

The Reference says /// Foo is the same as #[doc=" Foo"]: a doc comment is an attribute in a friendlier form. cargo doc --open turns doc comments into web pages and opens them (the Book). In a library crate (cargo new --lib), a code block in a doc comment is also a test that cargo test runs. Cargo takes doc tests from the library target only, so examples in a binary crate’s main.rs are not run.

What you write today, in Rust

Each row is the closest match, not an exact one.

Tidying: cargo fmt and cargo clippy

cargo fmt rewrites your files in the standard layout, so teams do not argue about style. cargo clippy runs extra lints on top of the compiler’s. The Book says standard Rust installations include both (with rustup’s minimal profile, add them with rustup component add rustfmt clippy). In a project folder you type cargo fmt or cargo clippy. The two programs below run the tools and print what they returned; you need not read the code, and clippy’s Checking and Finished lines are left out. rustfmt reads standard input when given no file:

Rustsrc/main.rsRun ↗
use std::io::Write;
use std::process::{Command, Stdio};
fn main() {
let messy = r#"fn area(w:u32,h:u32)->u32{
let a=w*h;
if a>100 {println!("big");}
a
}
struct Point{x:i32,y:i32}
"#;
// With no file name, `rustfmt` reads source from stdin and writes the result to stdout.
let mut child = Command::new("rustfmt")
.args(["--edition", "2024"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(messy.as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
println!(
"--- before ---\n{messy}--- after ---\n{}",
String::from_utf8_lossy(&out.stdout)
);
}
$ cargo run
--- before ---
fn area(w:u32,h:u32)->u32{
let a=w*h;
  if a>100 {println!("big");}
        a
}
struct Point{x:i32,y:i32}
--- after ---
fn area(w: u32, h: u32) -> u32 {
    let a = w * h;
    if a > 100 {
        println!("big");
    }
    a
}
struct Point {
    x: i32,
    y: i32,
}

Clippy’s default lints mostly warn, but the correctness group is an error (the Book’s approx_constant example). These warnings are about code that works. The first links to Statements and expressions: the last expression is the value, so a final return is noise.

Rustsrc/main.rsRun ↗
use std::fs;
use std::process::Command;
const SOURCE: &str = r#"fn double(x: i32) -> i32 {
return x * 2;
}
fn no_items(items: &[i32]) -> bool {
items.len() == 0
}
fn main() {
println!("{} {}", double(4), no_items(&[]));
}
"#;
fn main() {
// A throwaway Cargo project, so that `cargo clippy` has something to check.
let dir = std::env::temp_dir().join("clippy-demo");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("src")).unwrap();
let manifest = "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = \"2024\"\n";
fs::write(dir.join("Cargo.toml"), manifest).unwrap();
fs::write(dir.join("src/main.rs"), SOURCE).unwrap();
let out = Command::new("cargo")
.arg("clippy")
.current_dir(&dir)
.output()
.unwrap();
for line in String::from_utf8_lossy(&out.stderr).lines() {
if !line.trim_start().starts_with("Checking") && !line.trim_start().starts_with("Finished")
{
println!("{line}");
}
}
}
$ cargo run
warning: unneeded `return` statement
 --> src/main.rs:2:5
  |
2 |     return x * 2;
  |     ^^^^^^^^^^^^
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#needless_return
  = note: `#[warn(clippy::needless_return)]` on by default
help: remove `return`
  |
2 -     return x * 2;
2 +     x * 2
  |

warning: length comparison to zero
 --> src/main.rs:6:5
  |
6 |     items.len() == 0
  |     ^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `items.is_empty()`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#len_zero
  = note: `#[warn(clippy::len_zero)]` on by default

warning: `demo` (bin "demo") generated 2 warnings (run `cargo clippy --fix --bin "demo" -p demo -- ` to apply 2 suggestions)

Going deeper

Writing a macro: macro_rules!

macro_rules! lets you write your own macro. A parameter such as $x:expr stands for one whole parsed expression, and a variable the macro creates is separate from the caller’s. The Reference calls this “mixed-site hygiene”: loop labels, block labels and local variables are looked up where the macro was defined, other names where it is called. Hygiene does not stop a macro evaluating its argument once per use (twice here), as a function would not:

Rustsrc/main.rsRun ↗
macro_rules! square {
($x:expr) => {
$x * $x
};
}
macro_rules! double_into {
($dst:ident, $src:expr) => {
let tmp = $src * 2;
$dst = tmp;
};
}
fn square_fn(x: i32) -> i32 {
x * x
}
fn main() {
println!("square!(1 + 2) = {}", square!(1 + 2));
let mut tmp = 0;
println!("tmp before: {tmp}");
double_into!(tmp, 5);
println!("tmp after double_into!(tmp, 5) = {tmp}");
let mut i = 0;
let r = square!({
i += 1;
i
});
println!("square!({{ i += 1; i }}) = {r}, and i = {i}");
let mut j = 0;
let r = square_fn({
j += 1;
j
});
println!("square_fn(..) = {r}, and j = {j}");
}
$ cargo run
square!(1 + 2) = 9
tmp before: 0
tmp after double_into!(tmp, 5) = 10
square!({ i += 1; i }) = 2, and i = 2
square_fn(..) = 1, and j = 1

In double_into!(tmp, 5) the macro’s tmp and the caller’s tmp are different variables, so the caller’s is set to 10. Macros you’ll read shows how to read a macro_rules! block.

assert! is always on

The std docs say of assert!: “Assertions are always checked in both debug and release builds, and cannot be disabled.” debug_assert! is the version that is off in an optimised build by default. The debug_assertions setting that cfg.rs printed is what switches it on (Reference).

Practice

Try it

Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed.

The first one asks you to read rather than write.

Read this: what does it print?

Six format! lines, a #[derive(Debug)] and a pair of #[cfg] functions. Write the exact string each line produces, spaces included; the checks run the real code and name the line you got wrong, not the right text. #[cfg(not(test))] is the opposite of #[cfg(test)]: it keeps the function when test is off, as in a normal run. Numbers and text align differently, and a doubled brace is one brace.

Starterexercises/read-output.rs
// Read the format strings and attributes, then write what each line produces, spaces
// included. Replace the `todo!()` and run until the checks print `all checks passed`.
//
// `#[cfg(test)]` keeps the next item only in a test build; `#[cfg(not(test))]` is the
// opposite and keeps it when `test` is off, which is the case in a normal run.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
#[cfg(test)]
fn build() -> &'static str {
"test"
}
#[cfg(not(test))]
fn build() -> &'static str {
"normal"
}
fn shown() -> [String; 6] {
let p = Point { x: 3, y: -4 };
let q = p.clone();
[
format!("{:5}|{:5}|", 42, "ab"),
format!("{:05}|{:+}|{:#x}", 42, 42, 255),
format!("{:.2}|{:8.3}|", 3.14159, 3.14159),
format!("{{}}{}", 7),
format!("{p:?}"),
format!("{} {}", p == q, build()),
]
}
/// Hint, one per line:
/// 1. a number sits at the right of its width, text at the left.
/// 2. `0` pads with zeros, `+` shows the sign, `#x` is hex with a `0x` prefix.
/// 3. `.2` is two decimals; `8.3` is a width of 8 and three decimals.
/// 4. a doubled brace is one brace.
/// 5. `:?` is the Debug form, which a derive writes as `Name { field: value }`.
/// 6. `==` gives `true` or `false`; `build()` is the `cfg(not(test))` one in a normal run.
fn answers() -> [&'static str; 6] {
todo!(
"write the six strings, in order: numbers and text align differently, a doubled brace is one brace, the `:?` form is Debug, and `cfg(test)` is off in a normal run"
)
}
fn main() {
let (shown, answers) = (shown(), answers());
for i in 0..6 {
assert!(
answers[i] == shown[i],
"line {} of `shown`: you wrote {:?}",
i + 1,
answers[i]
);
println!("ok: line {} is {:?}", i + 1, answers[i]);
}
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/read-output.solution.rsRun ↗
// Read the format strings and attributes, then write what each line produces, spaces
// included. Replace the `todo!()` and run until the checks print `all checks passed`.
//
// `#[cfg(test)]` keeps the next item only in a test build; `#[cfg(not(test))]` is the
// opposite and keeps it when `test` is off, which is the case in a normal run.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
#[cfg(test)]
fn build() -> &'static str {
"test"
}
#[cfg(not(test))]
fn build() -> &'static str {
"normal"
}
fn shown() -> [String; 6] {
let p = Point { x: 3, y: -4 };
let q = p.clone();
[
format!("{:5}|{:5}|", 42, "ab"),
format!("{:05}|{:+}|{:#x}", 42, 42, 255),
format!("{:.2}|{:8.3}|", 3.14159, 3.14159),
format!("{{}}{}", 7),
format!("{p:?}"),
format!("{} {}", p == q, build()),
]
}
/// Hint, one per line:
/// 1. a number sits at the right of its width, text at the left.
/// 2. `0` pads with zeros, `+` shows the sign, `#x` is hex with a `0x` prefix.
/// 3. `.2` is two decimals; `8.3` is a width of 8 and three decimals.
/// 4. a doubled brace is one brace.
/// 5. `:?` is the Debug form, which a derive writes as `Name { field: value }`.
/// 6. `==` gives `true` or `false`; `build()` is the `cfg(not(test))` one in a normal run.
fn answers() -> [&'static str; 6] {
[
" 42|ab |",
"00042|+42|0xff",
"3.14| 3.142|",
"{}7",
"Point { x: 3, y: -4 }",
"true normal",
]
}
fn main() {
let (shown, answers) = (shown(), answers());
for i in 0..6 {
assert!(
answers[i] == shown[i],
"line {} of `shown`: you wrote {:?}",
i + 1,
answers[i]
);
println!("ok: line {} is {:?}", i + 1, answers[i]);
}
println!("all checks passed");
}
$ cargo run
ok: line 1 is "   42|ab   |"
ok: line 2 is "00042|+42|0xff"
ok: line 3 is "3.14|   3.142|"
ok: line 4 is "{}7"
ok: line 5 is "Point { x: 3, y: -4 }"
ok: line 6 is "true normal"
all checks passed

Braces in the output

Return a JSON object with one entry, such as {"age": 36}. The braces of the object are text, not placeholders. The last check passes a key that contains braces of its own.

Starterexercises/json-pair.rs
/// Build the text of a one-entry JSON object, such as `{"age": 36}`.
/// Hint: in a format string, `{{` prints one `{` and `}}` prints one `}`.
fn json_pair(key: &str, value: i32) -> String {
todo!("write the braces twice in the format string to print one, and put the key in quotes")
}
fn main() {
assert_eq!(json_pair("age", 36), "{\"age\": 36}");
assert_eq!(json_pair("score", -5), "{\"score\": -5}");
// The case to watch: braces inside an argument are printed as they are.
assert_eq!(json_pair("{x}", 1), "{\"{x}\": 1}");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/json-pair.solution.rsRun ↗
/// Build the text of a one-entry JSON object, such as `{"age": 36}`.
/// Hint: in a format string, `{{` prints one `{` and `}}` prints one `}`.
fn json_pair(key: &str, value: i32) -> String {
format!("{{\"{key}\": {value}}}")
}
fn main() {
assert_eq!(json_pair("age", 36), "{\"age\": 36}");
assert_eq!(json_pair("score", -5), "{\"score\": -5}");
// The case to watch: braces inside an argument are printed as they are.
assert_eq!(json_pair("{x}", 1), "{\"{x}\": 1}");
println!("all checks passed");
}
$ cargo run
all checks passed

A receipt row

Format a name, a quantity and a price into fixed-width columns with one format!. The format table above has every spec you need. The last two checks use a value wider than its column and a name with an accented letter.

Starterexercises/price-row.rs
/// One line of a receipt, in three columns separated by `|`:
/// the name, left-aligned in a column 10 wide;
/// the quantity, right-aligned in a column 3 wide;
/// the price with 2 decimals, right-aligned in a column 8 wide.
fn row(name: &str, qty: u32, price: f64) -> String {
todo!(
"one format! call: the number after the colon is the width, `<` or `>` before it picks the side, and `.N` sets the decimals"
)
}
fn main() {
assert_eq!(row("apple", 3, 1.5), "apple | 3| 1.50");
assert_eq!(row("melon", 12, 1234.5), "melon | 12| 1234.50");
// The case to watch: a value wider than its column is not cut off.
assert_eq!(
row("watermelon-xl", 1000, 2.0),
"watermelon-xl|1000| 2.00"
);
// Width counts characters, not bytes: "crème" is 5 characters (6 bytes).
assert_eq!(row("crème", 1, 0.5), "crème | 1| 0.50");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/price-row.solution.rsRun ↗
/// One line of a receipt, in three columns separated by `|`:
/// the name, left-aligned in a column 10 wide;
/// the quantity, right-aligned in a column 3 wide;
/// the price with 2 decimals, right-aligned in a column 8 wide.
fn row(name: &str, qty: u32, price: f64) -> String {
format!("{:<10}|{:>3}|{:>8.2}", name, qty, price)
}
fn main() {
assert_eq!(row("apple", 3, 1.5), "apple | 3| 1.50");
assert_eq!(row("melon", 12, 1234.5), "melon | 12| 1234.50");
// The case to watch: a value wider than its column is not cut off.
assert_eq!(
row("watermelon-xl", 1000, 2.0),
"watermelon-xl|1000| 2.00"
);
// Width counts characters, not bytes: "crème" is 5 characters (6 bytes).
assert_eq!(row("crème", 1, 0.5), "crème | 1| 0.50");
println!("all checks passed");
}
$ cargo run
all checks passed

One line or many

Return the Debug text of an optional point in its one-line or its multi-line form. Afterwards, delete the #[derive(Debug)] line to see the error from this page, then put it back.

Starterexercises/debug-text.rs
#[derive(Debug)]
#[allow(dead_code)] // the fields are only printed, never read
struct Point {
x: i32,
y: i32,
}
/// The `Debug` text of an optional point: on one line, or spread over
/// several lines when `pretty` is true.
fn show(p: &Option<Point>, pretty: bool) -> String {
todo!("format! can print a value in its one-line Debug form or its multi-line form")
}
fn main() {
let some = Some(Point { x: 1, y: -2 });
assert_eq!(show(&some, false), "Some(Point { x: 1, y: -2 })");
assert_eq!(
show(&some, true),
"Some(\n Point {\n x: 1,\n y: -2,\n },\n)"
);
// The case to watch: there may be no point at all.
assert_eq!(show(&None, false), "None");
assert_eq!(show(&None, true), "None");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/debug-text.solution.rsRun ↗
#[derive(Debug)]
#[allow(dead_code)] // the fields are only printed, never read
struct Point {
x: i32,
y: i32,
}
/// The `Debug` text of an optional point: on one line, or spread over
/// several lines when `pretty` is true.
fn show(p: &Option<Point>, pretty: bool) -> String {
if pretty {
format!("{:#?}", p)
} else {
format!("{:?}", p)
}
}
fn main() {
let some = Some(Point { x: 1, y: -2 });
assert_eq!(show(&some, false), "Some(Point { x: 1, y: -2 })");
assert_eq!(
show(&some, true),
"Some(\n Point {\n x: 1,\n y: -2,\n },\n)"
);
// The case to watch: there may be no point at all.
assert_eq!(show(&None, false), "None");
assert_eq!(show(&None, true), "None");
println!("all checks passed");
}
$ cargo run
all checks passed

Is it a hex digit?

Write is_hex_digit with matches!. A pattern (Match) can be a range such as 'a'..='f', and | joins alternatives. The last checks use look-alike digits and letters from other alphabets.

Starterexercises/is-hex.rs
/// True if `c` is one of the characters 0-9, a-f or A-F.
/// Hint: `matches!(value, pattern)` is true when the value fits the pattern.
/// A pattern can be a range such as `'a'..='f'`, and `|` means "or".
fn is_hex_digit(c: char) -> bool {
todo!("matches! with three ranges joined by |")
}
fn main() {
for c in ['0', '9', 'a', 'f', 'F'] {
assert!(is_hex_digit(c), "{c} should be a hex digit");
}
for c in ['g', 'G', ' ', '/'] {
assert!(!is_hex_digit(c), "{c} should not be a hex digit");
}
// The case to watch: look-alike digits and letters from other alphabets.
assert!(!is_hex_digit('٣'), "an Arabic-Indic digit is not 0-9");
assert!(!is_hex_digit('a'), "a full-width letter is not a-f");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/is-hex.solution.rsRun ↗
/// True if `c` is one of the characters 0-9, a-f or A-F.
/// Hint: `matches!(value, pattern)` is true when the value fits the pattern.
/// A pattern can be a range such as `'a'..='f'`, and `|` means "or".
fn is_hex_digit(c: char) -> bool {
matches!(c, '0'..='9' | 'a'..='f' | 'A'..='F')
}
fn main() {
for c in ['0', '9', 'a', 'f', 'F'] {
assert!(is_hex_digit(c), "{c} should be a hex digit");
}
for c in ['g', 'G', ' ', '/'] {
assert!(!is_hex_digit(c), "{c} should not be a hex digit");
}
// The case to watch: look-alike digits and letters from other alphabets.
assert!(!is_hex_digit('٣'), "an Arabic-Indic digit is not 0-9");
assert!(!is_hex_digit('a'), "a full-width letter is not a-f");
println!("all checks passed");
}
$ cargo run
all checks passed

Read more