5.1Constants and statics

Last updated June 12, 2026

Your falling-ball program from the chapter 4 quiz contains the line let fallen = 9.8 * seconds * seconds / 2.0;. That 9.8 is the acceleration due to gravity, in meters per second squared, and it has been the same number since long before anyone wrote programs about it. Storing it in an ordinary variable would work, but an ordinary variable undersells the situation: nothing about let gravity = 9.8; tells the reader (or the compiler) that this value is never supposed to change. Rust has a dedicated tool for exactly this, and lesson 1.8 promised it: constants, the kind with a naming convention of their own. The ball's 9.8 is about to get a name.

Declaring a constant

A constant is a named value fixed for the entire life of the program, declared with the const keyword:

const EARTH_GRAVITY: f64 = 9.8;

fn fall_distance(seconds: f64) -> f64 {
    EARTH_GRAVITY * seconds * seconds / 2.0
}

fn main() {
    println!("After 3 seconds, a dropped ball has fallen {} meters.", fall_distance(3.0));
    println!("Earth's gravity is {EARTH_GRAVITY} m/s^2.");
}
After 3 seconds, a dropped ball has fallen 44.1 meters.
Earth's gravity is 9.8 m/s^2.

Three things in that program are new. First, the syntax: const, a name, a type annotation, a value. Second, the location: the constant lives outside any function, the first time this course has put anything out there, and both functions can see it. Constants are allowed in any scope, including that outermost one (called global scope); a constant only needed inside one function can be declared inside it.

Third, the annotation : f64 isn't a courtesy. For constants, the type is required, and the compiler holds the line:

const EARTH_GRAVITY = 9.8;

fn main() {
    println!("{EARTH_GRAVITY}");
}
error: missing type for `const` item
 --> src/main.rs:1:20
  |
1 | const EARTH_GRAVITY = 9.8;
  |                    ^ help: provide a type for the constant: `: f64`

Lesson 4.8 explained why signatures stay explicit while locals get inferred: things visible from far away should say what they are. A global constant is about as visible-from-far-away as Rust gets, so it documents itself, by law.

Known at compile time

A constant's value must be something the compiler can work out while compiling, before the program ever runs. Literals qualify, and so does arithmetic on literals and other constants:

const SECONDS_PER_MINUTE: u32 = 60;
const SECONDS_PER_HOUR: u32 = SECONDS_PER_MINUTE * 60;
const SECONDS_PER_DAY: u32 = SECONDS_PER_HOUR * 24;

fn main() {
    println!("A day has {SECONDS_PER_DAY} seconds.");
}
A day has 86400 seconds.

The multiplication happens at compile time; the finished program contains 86400 and does no arithmetic at all. You've actually watched this compile-time evaluation machinery at work twice already, both times as a refusal: lesson 3.1's provable divide-by-zero and lesson 4.4's provable overflow were both rejected at compile time because the compiler computed the constant expressions and didn't like what it found.

What can't be a constant is anything only knowable at runtime:

fn main() {
    let height = 100;
    const TOWER_HEIGHT: i32 = height;
    println!("{TOWER_HEIGHT}");
}
error[E0435]: attempt to use a non-constant value in a constant
 --> src/main.rs:3:31
  |
3 |     const TOWER_HEIGHT: i32 = height;
  |                               ^^^^^^ non-constant value
  |
help: consider using `let` instead of `const`
  |
3 -     const TOWER_HEIGHT: i32 = height;
3 +     let TOWER_HEIGHT: i32 = height;
  |

height is an ordinary variable, its value a runtime matter (trivially so here, but the rule doesn't read minds), and const won't have it. This is the real division of labor: an immutable let is "computed while running, then frozen"; a const is "settled before the program exists." User input, for instance, can never be const, no matter how immutable it is once read.

Author's note

If you're arriving from C++: this lesson is suspiciously short, and that's the point. C++'s const doesn't guarantee compile-time evaluation, so learncpp needs a three-lesson arc (the as-if rule, constant expressions, the constexpr keyword) to build up to a variable that's reliably a compile-time constant. Rust's const is constexpr by birth. The entire arc collapses into the paragraph you just read.

Key insight

Every value that can change is a moving part, something a reader must track and a bug can hide behind. Constants and immutable let bindings are inert: once you've read the declaration, you're done thinking about them. This is why Rust made immutable the default (lesson 1.4) and why you should reach for const whenever a value is truly fixed: it's one less moving part, and it announces itself.

Naming: the shouting convention

Lesson 1.8 promised that constants get SCREAMING_SNAKE_CASE, and that the compiler nudges you about conventions personally. Time to collect on both:

const earth_gravity: f64 = 9.8;

fn main() {
    println!("{earth_gravity}");
}
warning: constant `earth_gravity` should have an upper case name
 --> src/main.rs:1:7
  |
1 | const earth_gravity: f64 = 9.8;
  |       ^^^^^^^^^^^^^
  |
  = note: `#[warn(non_upper_case_globals)]` (part of `#[warn(nonstandard_style)]`) on by default
help: convert the identifier to upper case
  |
1 - const earth_gravity: f64 = 9.8;
1 + const EARTH_GRAVITY: f64 = 9.8;
  |

The program compiles and runs, but the warning is right and you should obey it. The shouting is a feature: when MAX_PLAYERS appears in the middle of a function, the capitals announce "fixed value, declared elsewhere, don't look for an assignment" before you've consciously read the name.

Magic numbers

Constants also fix a code smell you've already produced (the course made you do it, in fairness). A magic number is a bare literal whose meaning isn't obvious from context. Consider a school program:

fn main() {
    let classrooms = 4;
    let max_students = classrooms * 30;
    println!("The school can hold {max_students} students.");
    println!("Hiring {} teachers.", 4 * 30 / 15);
}

What's 30? Students per classroom, probably. Is the 30 on line 5 the same thirty, or a different fact that happens to share a digit pattern? If the classroom size changes to 25, how many of these numbers do you edit? You can't tell without re-deriving the entire program in your head, and neither can the colleague who inherits it. Named constants delete the guesswork:

const STUDENTS_PER_CLASSROOM: i32 = 30;
const STUDENTS_PER_TEACHER: i32 = 15;

fn main() {
    let classrooms = 4;
    let max_students = classrooms * STUDENTS_PER_CLASSROOM;
    println!("The school can hold {max_students} students.");
    println!("Hiring {} teachers.", max_students / STUDENTS_PER_TEACHER);
}

Now every number states its meaning, the duplicated fact lives in exactly one place, and a policy change is a one-line edit. Note the second println! also got more honest: writing it in terms of max_students exposed that it always depended on the same quantity.

Not every literal is magic. Obvious conversion factors (* 1000.0 for kilometers to meters), the 2.0 in a halving, a loop's starting 0: these explain themselves, and naming them (const TWO: f64 = 2.0; is a real thing people have committed) adds noise, not clarity.

Best practice

Give a name to any literal whose meaning isn't instantly obvious, especially if it appears more than once. Prefer const for the name, at the narrowest scope that covers all its uses.

The two paragraphs static deserves

Rust has a second keyword for fixed global values: static. The declaration looks like const with the keyword swapped (static GREETING: &str = "hello";), and the difference is about identity. A const has no fixed home in memory; the compiler copies its value into every place it's used, the way 86400 got baked in earlier. A static is the opposite: one value, at one address, for the program's whole life, and every mention refers to that single home.

When would the address matter? Not yet, and not for a while: the honest examples involve interior mutability (chapter 21) and talking to other languages (chapter 25). Until a situation actively demands one home in memory, the working rule is short: use const, and read static in other people's code as "like a const, but it has an address."

Quiz time

Question #1

For each value, the best declaration: const, immutable let, or let mut?

a) the number of milliseconds in a second b) a player's current score c) the tower height typed in by the user, used but never changed afterward d) the maximum number of login attempts your program allows

Show solution

a) const: fixed forever, known while writing the program. b) let mut: scores exist to change. c) immutable let: never changes once read, but it arrives at runtime, so const is impossible (the compiler will say so, with E0435). d) const: it's a policy you decided at your desk, exactly the "settled before the program exists" case. Name it like MAX_LOGIN_ATTEMPTS and the capitals will do the explaining.

Question #2

This program works. Improve it without changing its output:

fn main() {
    let baskets = 3;
    println!("Apples: {}", baskets * 12);
    println!("If one basket spoils: {}", (baskets - 1) * 12);
}
Show solution
const APPLES_PER_BASKET: i32 = 12;

fn main() {
    let baskets = 3;
    println!("Apples: {}", baskets * APPLES_PER_BASKET);
    println!("If one basket spoils: {}", (baskets - 1) * APPLES_PER_BASKET);
}

The 12 appeared twice with no hint that both meant "apples per basket," and a change to basket size would have required finding every copy. The 1 in baskets - 1, by contrast, is self-explanatory ("one fewer basket") and stays a literal.

Question #3

Without compiling: what does the compiler say about this program, and does it run?

const max_score: u32 = 100;

fn main() {
    println!("Top score: {max_score}");
}
Show solution

It compiles and runs (printing Top score: 100), but the compiler issues a warning: constant max_score should have an upper case name, with a help line offering MAX_SCORE and a note that #[warn(non_upper_case_globals)] is on by default. Conventions aren't errors, but lesson 0.11's policy stands: a warning is a to-do item, not a decoration.

One question this lesson raised quietly: if let bindings are immutable, how has let trimmed = name.trim(); been making new variables for the same data since lesson 1.6, and is there a tidier way? There is, it has a name, and it's next.