Constants & Naming Conventions

Python has no true, enforced constant — 'constants' are a naming convention (UPPER_SNAKE_CASE) backed at most by typing-level hints, and this topic is careful to separate that convention from actual object immutability.

Many languages have a dedicated keyword — const, final, readonly — that makes the compiler reject any attempt to reassign a value. Python has no such keyword. What Python developers call a "constant" is purely a naming convention, optionally reinforced by static type-checking tools, but never enforced by the interpreter at runtime. This topic explains exactly what that means, and carefully separates "this name is conventionally treated as constant" from "this object is immutable" — two genuinely different concepts that are easy to conflate.

Python Has No True, Enforced Constants

Unlike C++'s const, Java's final, or JavaScript's const, Python provides no language keyword that prevents a name from being reassigned. Any name — however it's written — can be rebound at any time; the interpreter never rejects a reassignment based on some prior "constant" declaration, because no such declaration exists at the language level.

MAX_RETRIES = 5
MAX_RETRIES = 10   # perfectly legal — nothing about the ALL_CAPS name stops this

The UPPER_SNAKE_CASE Convention

Since the language provides no enforcement, Python's community developed a naming convention instead, formalized in PEP 8: names intended to be treated as constants — values that should not be reassigned after their initial definition — are written in UPPER_SNAKE_CASE (all uppercase, words separated by underscores). This is purely a signal to human readers (and to some linting tools) that "this value is not meant to change"; it carries zero runtime enforcement.

MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159265358979

Module Constants

The most common place to define constants is at module level — plain assignments near the top of a file, after imports, conventionally before any class or function definitions (see the Program Structure topic's file-organization guidance). Other modules that import this one can read these values, though — again — nothing prevents any importer from reassigning them if they really try.

# config.py
MAX_UPLOAD_SIZE_MB = 25
SUPPORTED_FORMATS = ("jpg", "png", "gif")
API_BASE_URL = "https://api.example.com/v1"
# main.py
import config

print(config.MAX_UPLOAD_SIZE_MB)  # 25 — read-only in spirit, not in enforcement

The Final Typing Concept

Python's typing module offers Final (introduced in Python 3.8, via PEP 591), a type annotation that tells static type checkers — like mypy or Pyright, both third-party/ecosystem tools — that a name should never be reassigned. This is enforced only by those external tools during static analysis; it has zero effect on the running interpreter, which still allows reassignment without complaint.

Final only adds value if your workflow actually runs a static type checker as part of development or CI. Without one, Final: int annotations are documentation only — informative to readers, invisible to the interpreter.

from typing import Final

MAX_RETRIES: Final[int] = 5
MAX_RETRIES = 10   # a type checker like mypy would FLAG this as an error;
                     # the Python interpreter itself runs this line without any complaint

Immutable Objects versus Constants — Not the Same Thing

This is the most important distinction in this topic. "Constant" describes a promise about a NAME (that it won't be reassigned). "Immutable" describes a property of an OBJECT's type (that its value/state cannot be changed in place once created). These are independent: a constant-by-convention name can point at a mutable object, and an immutable object can be referenced by a name that gets reassigned all the time.

Naming something a "constant" says nothing about whether its contents can change. To get a value that is both conventionally constant AND structurally protected against in-place mutation, choose an immutable type in the first place (a tuple instead of a list, or frozenset instead of set).

DEFAULT_TAGS = ["urgent", "review"]   # UPPER_SNAKE_CASE name, but points at a MUTABLE list

DEFAULT_TAGS.append("new")   # legal — the list object itself can still be mutated in place,
                                # even though the NAME "DEFAULT_TAGS" was never reassigned
print(DEFAULT_TAGS)           # ['urgent', 'review', 'new']
DEFAULT_TAGS: Final[tuple[str, ...]] = ("urgent", "review")
# Now: the name is conventionally constant (UPPER_SNAKE_CASE + Final),
# AND the object itself is immutable (a tuple) — no append(), no in-place mutation possible at all.

Naming Practices for Constants

Beyond casing, a few practical conventions round out how Python constants are typically written: group related constants together near the top of a module; use descriptive names that convey units or meaning (TIMEOUT_SECONDS, not just TIMEOUT); prefer immutable container types (tuple, frozenset) for constant collections so the "don't mutate this" intent is structurally reinforced, not just named; and, for a constant scoped to a class rather than a module, define it as a class attribute in UPPER_SNAKE_CASE.

class HttpStatus:
    """Common HTTP status codes used throughout this project."""
    OK = 200
    NOT_FOUND = 404
    SERVER_ERROR = 500

print(HttpStatus.NOT_FOUND)  # 404

Complete Example

"""Application-wide configuration constants."""

from typing import Final

MAX_LOGIN_ATTEMPTS: Final[int] = 3
SESSION_TIMEOUT_MINUTES: Final[int] = 15
SUPPORTED_LOCALES: Final[tuple[str, ...]] = ("en", "fr", "es", "de")


def is_locale_supported(locale: str) -> bool:
    """Return True if locale is one of SUPPORTED_LOCALES."""
    return locale in SUPPORTED_LOCALES


if __name__ == "__main__":
    print(is_locale_supported("fr"))   # True
    print(is_locale_supported("jp"))   # False
    # SUPPORTED_LOCALES = ["en"]  # would silently run at the interpreter level,
                                    # but a type checker configured to respect Final
                                    # would flag it as an error before you ever ship it

Key Takeaways

Python has no interpreter-enforced constant keyword — every name can be reassigned, always.

UPPER_SNAKE_CASE is a naming convention (from PEP 8) signaling 'treat this as constant'; it is not checked or enforced by the interpreter.

typing.Final tells external static type checkers (mypy, Pyright — third-party tools) to flag reassignment; it has no runtime effect on its own.

'Constant' (a promise about a name) and 'immutable' (a property of an object's type) are different concepts — a constant-named list can still be mutated in place.

For a value that should neither be reassigned nor mutated, combine the UPPER_SNAKE_CASE/Final convention with an inherently immutable type like tuple or frozenset.

MAX_USERS = 100 #... MAX_USERS = 50 # silently allowed; nothing warns you this "shouldn't" happen from typing import Final MAX_USERS: Final[int] = 100 # a static type checker (mypy/Pyright) will flag any later reassignment as an error

Without Final and a type checker actually running in your workflow, nothing detects an accidental reassignment of a value meant to be constant — the UPPER_SNAKE_CASE name alone is not enforced.

DEFAULT_TAGS = ["urgent"] # named like a constant... DEFAULT_TAGS.append("late") # ...but silently mutated in place anyway

DEFAULT_TAGS: Final[tuple[str, ...]] = ("urgent",) # a genuinely immutable container

UPPER_SNAKE_CASE naming says nothing about whether the underlying object can be mutated in place — only choosing an immutable type (tuple, frozenset) actually prevents in-place changes.

Final has zero effect on the running interpreter by itself — it only produces an error if a separate static type-checking tool is actually run against the code.

Does Python have a keyword like const or final that prevents a variable from being reassigned at runtime? What does it have instead?

No, Python has no such keyword — any name can always be reassigned. What it has instead is a naming convention (UPPER_SNAKE_CASE, from PEP 8) to signal intent, and optionally the typing.Final annotation, which only static type-checking tools like mypy enforce, not the interpreter itself.

If MAX_RETRIES = 5 is written in a module, does anything stop later code in the same program from executing MAX_RETRIES = 10?

No. The UPPER_SNAKE_CASE naming is purely conventional; the Python interpreter treats MAX_RETRIES exactly like any other name and allows reassignment without any warning or error.

What does typing.Final actually do at runtime versus during static analysis?

At runtime, absolutely nothing — the interpreter ignores it as far as enforcement goes and allows reassignment to proceed normally. During static analysis, a type checker like mypy or Pyright (both third-party tools) that is configured to check the code will flag any reassignment of a Final-annotated name as an error before the program ever runs.

Explain why SETTINGS = {"debug": False} followed by SETTINGS["debug"] = True does not violate anything about SETTINGS being named like a constant.

The name SETTINGS itself was never reassigned — it still refers to the exact same dict object throughout. Only the dict's internal contents were mutated in place, which UPPER_SNAKE_CASE naming says nothing about preventing; naming conventions govern reassignment of the name, not mutation of the object it points to.

If you want a value that is both conventionally constant AND cannot be mutated in place, what should you do differently from just naming it in UPPER_SNAKE_CASE?

Choose an inherently immutable type for the value itself — for example a tuple instead of a list, or a frozenset instead of a set — in addition to the UPPER_SNAKE_CASE naming (and optionally a Final annotation checked by a type checker). The immutability has to come from the object's type, not from how the name is capitalized.

Is it accurate to say 'constants in Python are immutable'? Why is this phrasing misleading?

It's misleading because it conflates two different things: a 'constant' is a naming/intent convention about a NAME not being reassigned, while 'immutable' is a property of an OBJECT's type. A name written like a constant can perfectly well point at a mutable object (like a list), which can still be changed in place even though the name itself is never reassigned.

What does this print when actually run with the Python interpreter (not a type checker)?

It prints 3.14. Final has no effect on the running interpreter by itself — only a separate static type-checking tool (like mypy or Pyright) configured to check this code would flag the reassignment as an error; plain python execution allows it silently.

Write a module-level constant COLORS as an immutable tuple of the strings 'red', 'green', 'blue', annotated with typing.Final.

from typing import Final COLORS: Final[tuple[str, ...]] = ("red", "green", "blue")

This combines the UPPER_SNAKE_CASE naming convention, a Final annotation (checked only by external type-checking tools), and an inherently immutable tuple type — the strongest practical form of a Python 'constant.'

Does this violate DEFAULTS being 'constant'? What does it print?

It prints {'retries': 5}. This does not reassign the name DEFAULTS at all — it mutates the dict object in place through item assignment. Since UPPER_SNAKE_CASE naming only conventionally discourages reassigning the NAME, not mutating the object, this is fully consistent with (if perhaps confusingly at odds with the spirit of) calling DEFAULTS a "constant."

In one or two sentences, explain the difference between a 'constant' and an 'immutable object' in Python.

A 'constant' is a convention about a NAME — that it is not intended to be reassigned to a different object — enforced only by naming style and, optionally, external type checkers via Final. An 'immutable object' is a property of the OBJECT itself — its type (like int, str, or tuple) guarantees its value/state can never be changed in place once created, regardless of how many names point to it or whether those names get reassigned.

from typing import Final

PI: Final[float] = 3.14159
PI = 3.14
print(PI)
DEFAULTS = {"retries": 3}
DEFAULTS["retries"] = 5
print(DEFAULTS)