In Python, the concept of a “constant” is not enforced by the language syntax. Unlike languages such as C++ or Java, Python does not have a built-in keyword to declare an immutable variable whose value cannot be changed after its initial definition. This design choice aligns with Python’s “we are all consenting adults” philosophy, which trusts developers not to modify values that are intended to be fixed. Instead, Python offers two primary, complementary mechanisms to communicate the intent of a constant: a strong naming convention and optional static type hints.

The Naming Convention for Constants

The universally adopted convention to signify a constant is to use a variable name in all uppercase letters with underscores separating words. This convention is not a technical constraint—the interpreter will not prevent reassignment—but a powerful social contract. When a developer sees a variable like MAX_SPEED, they immediately understand that this value should be treated as read-only throughout the codebase. Modifying it would be considered a severe breach of best practices and would likely introduce subtle, hard-to-find bugs.

# Constants defined by convention
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30.5
API_BASE_URL = "https://api.example.com"
SUPPORTED_FORMATS = ["JSON", "XML"]

# This is technically possible but is a VERY BAD IDEA.
MAX_CONNECTIONS = 50  # This will break other parts of the code that rely on the original value.

The reason this convention works so well in practice is its clarity and universality. It is one of the first conventions taught to new Python programmers and is rigorously followed in all major Python projects and style guides like PEP 8.

Enforcing Intent with the Final Type Hint

While the naming convention is excellent for human readers, it provides no feedback from static type checkers or linters. To address this, Python’s typing module introduced the Final qualifier and the final decorator (the latter being for classes and methods). A Final type hint, introduced in PEP 591, is a way to explicitly declare that a variable should not be reassigned or overridden. Using this hint allows tools like mypy, pyright, or pyre to catch accidental reassignments before the code is even run.

from typing import Final

# Declaring a true constant with an explicit type
MAX_USERS: Final[int] = 1024

# This will be flagged as an error by a static type checker.
MAX_USERS = 512  # Error: Cannot assign to final name "MAX_USERS"

# Declaring a constant with an inferred type
API_KEY: Final = "a1b2c3d4"  # type inferred as Final[str]

It is crucial to understand that Final is only enforceable by static type checkers. The Python runtime does not enforce it. The following code will execute without any exception, demonstrating that Final is a hint for tooling, not a runtime feature:

from typing import Final

DB_HOST: Final[str] = "prod-db.example.com"
print(DB_HOST)  # Output: prod-db.example.com

# The runtime allows this, but a type checker will report a massive warning.
DB_HOST = "localhost"
print(DB_HOST)  # Output: localhost (This is a bug!)

The final Decorator for Classes and Methods

The final decorator is used to indicate that a class should not be subclassed or that a method should not be overridden. This is useful for creating robust library code where the author needs to guarantee certain behaviors remain unchanged.

from typing import final

@final
class BaseWidget:
    """This class is not designed to be subclassed."""
    ...

class CustomWidget(BaseWidget):  # Error: Cannot inherit from final class "BaseWidget"
    ...

Constants and Mutable Data Structures

A critical pitfall arises when a “constant” is assigned to a mutable object, such as a list or a dictionary. The Final hint only prevents reassignment of the variable itself; it does not make the contents of the referenced object immutable. This is a direct consequence of Python’s object model: the variable is a reference to an object. Final ensures the reference cannot change, but the object it points to can still be modified if it is mutable.

from typing import Final

SERVER_PORTS: Final[list[int]] = [8000, 8001, 8080]

# This reassignment is caught by type checkers.
SERVER_PORTS = [9000]  # Error: Cannot assign to final name "SERVER_PORTS"

# However, this mutation is NOT prevented and is a common source of bugs.
SERVER_PORTS.append(9000)  # No error! The list has been changed.
print(SERVER_PORTS)  # Output: [8000, 8001, 8080, 9000]

To prevent this, you should use immutable types for constants whenever possible. Instead of a list, use a tuple.

# Prefer immutable types for constants
SERVER_PORTS: Final[tuple[int, ...]] = (8000, 8001, 8080)
# SERVER_PORTS.append(9000)  # This would now raise an AttributeError at runtime

Best Practices and Summary

  1. Use Uppercase Naming Always: For any value meant to be constant, always use the UPPER_CASE_WITH_UNDERSCORES convention. This is your first and most important line of defense.
  2. Add Final Type Hints for Robustness: For critical constants, use the Final type hint. This leverages static analysis tools to catch accidental reassignments early in the development cycle.
  3. Prefer Immutable Types: To create truly constant values, use immutable objects like integers, strings, frozensets, or tuples. This protects the data itself from being altered, not just the variable name.
  4. Understand the Limits: Remember that Final is a static check, not a runtime guarantee. It will not stop deliberate misuse or runtime modification of mutable objects.
  5. Use the final Decorator for API Stability: Mark classes and methods that are not designed for inheritance with the @final decorator to prevent users of your code from accidentally creating fragile subclasses.

By combining the clear signaling of the naming convention with the mechanical enforcement of the Final type hint, Python programmers can effectively create and manage constants, leading to more predictable, maintainable, and error-resistant code.