Every mainstream language has a way to say “this binding cannot be reassigned” — and almost none of them agree on how. The keyword map below replaces the one from the original 2023 version of this post, which glossed over the real differences (claiming, among other things, that Python and Go have a let). This version lists the actual keywords per language.
The real keyword map
| Language | Immutable binding | Mutable binding | Notes |
|---|---|---|---|
| C | const |
plain declaration | const is read-only at runtime, not a compile-time constant — use enum or #define for real constants. |
| C++ | const, constexpr |
plain declaration | constexpr gives true compile-time constants; mutable carves exceptions out of const objects. |
| Java | final |
plain declaration (var for inference) |
var is Java 10+, local variables only — it says nothing about immutability. |
| Python | none — UPPER_CASE convention, typing.Final |
plain assignment | No declaration keywords at all; Final is a static hint, not enforced at runtime. |
| JavaScript | const |
let |
var is legacy: function-scoped and hoisted. Prefer const/let. |
| TypeScript | const |
let |
Adds readonly properties and as const for deep immutability. |
| PHP | const / define() |
plain $var |
Typed class constants arrived in PHP 8.3. |
| Ruby | capitalized identifier (MAX = 100) |
plain local variable | Reassignment warns instead of erroring; freeze protects the object itself. |
| Swift | let |
var |
|
| Kotlin | val |
var |
|
| C# | const (compile-time), readonly (runtime) |
plain declaration, var inference |
|
| Go | const |
var or := |
No let; constants are compile-time and untyped by default. |
| Rust | const, static |
let; let mut for mutable |
Bindings are immutable by default — the only mainstream language where this is true. |
| R | none (lockBinding()) |
<- assignment |
Constants are simulated with locked bindings. |
| MATLAB | none | plain assignment | The properties (Constant) class pattern is the closest thing. |
| Objective-C | const, often with extern |
plain declaration | #define is the legacy alternative. |
| Perl | use constant |
my $x |
my is lexical scoping, not immutability. |
| Scala | val |
var |
|
| Shell (bash) | readonly |
plain assignment | local is scoping and often mistaken for immutability. |
| SQL | none | DECLARE (T-SQL), @var (MySQL) |
Standard SQL has no session variables; dialects differ. |
| Lua | local x <const> (5.4+) |
local x or global |
The <const> attribute is new in Lua 5.4. |
| PowerShell | Set-Variable -Option Constant |
$var = ... |
No declaration keywords; everything is $name. |
| Visual Basic | Const |
Dim |
The gotchas worth knowing
- JavaScript/TypeScript:
constbinds the reference, not the value —const x = []still lets youx.push(). Deep immutability needsObject.freeze()(oras constin TypeScript). - Rust: immutability is the default, so
let mutis opt-in.constandstaticare compile-time items, not bindings. Shadowing lets you re-leta name with a new value — deliberately not the same thing as mutation. - C vs C++: in C,
constcreates a read-only object, not a compile-time constant (a C89 array can’t be sized with one). C++ addedconstexprfor the real thing. - C#:
constvalues are baked into consuming assemblies at compile time;readonlyfields are evaluated at runtime. Changing a publicconstrequires recompiling every consumer — preferstatic readonlyfor anything that might change. - Java:
finalis the whole story.var(Java 10+) is purely about local type inference and says nothing about mutability. - Python: immutability is convention (
UPPER_CASE) plus static hints —typing.Final(3.8+) and@dataclass(frozen=True). Nothing is enforced at runtime. - Ruby: constants are just capitalized identifiers; reassignment only warns, and the referenced object stays mutable unless you
freezeit. - Bash:
readonlyis the immutability keyword;localis scoping and gets confused for it.
Why let mut is Rust-only
The original title bundled let mut in with the other keywords because Rust makes it famous — but it is the odd one out precisely because Rust flips the default: everywhere else, variables are mutable unless you mark them immutable, while in Rust they are immutable unless you mark them mutable. Swift and Kotlin split the same job across let/var and val/var; Rust just spells the mutable side out.
Conclusion
There is no portable mental model for “the const keyword” — only per-language conventions. The reliable questions to ask of any new language are: which keyword makes a binding immutable, is immutability deep (the object) or shallow (the binding), and is the constant compile-time or runtime. The table above answers the first question for the 20+ languages most of us touch; the gotchas cover the other two where they bite.