Python has no const keyword. Constants are a convention — names in SCREAMING_SNAKE_CASE per PEP 8 (2001) — and the language trusts you not to reassign them. PEP 591 (Python 3.8, October 2019) added typing.Final, which makes static type checkers like mypy and Pyright flag rebinding at analysis time, with no runtime cost. For closed sets of values, the enum module (PEP 435, Python 3.4) and its StrEnum sibling (PEP 663, Python 3.11) replace the old "module of uppercase variables" pattern. This guide covers all three layers, the immutable collection types you should prefer, and the standard-library constants worth knowing by heart.
Why does Python use a naming convention instead of a keyword?
Python's design philosophy ("we are all consenting adults") favours convention over enforcement. PEP 8 codified SCREAMING_SNAKE_CASE for module-level constants, and modern linters like Ruff (rule PLR2004, "magic value used in comparison") and Pylint flag accidental reassignment of uppercase names. The convention is a strong social contract; the language itself does not stop you from doing MAX_RETRIES = 999 at runtime.
# Module-level constants — PEP 8 convention
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30.0
API_BASE_URL = "https://api.example.com/v1"
SUPPORTED_FORMATS = ("png", "jpg", "webp") # tuple, not list
# Nothing prevents this — but linters will complain
MAX_RETRIES = 999What does typing.Final actually do?
typing.Final is a static-type-checker hint. mypy, Pyright, and pytype see : Final and refuse to type-check any code that reassigns the name. There is no runtime check — at execution time, Final is just an annotation. Pair it with the naming convention to get the strongest signal: visual + tooling.
from typing import Final
MAX_CONNECTIONS: Final = 100
API_VERSION: Final[str] = "v2"
FEATURE_FLAGS: Final[dict[str, bool]] = {"new_ui": True, "beta_api": False}
# At runtime, this works (no enforcement):
MAX_CONNECTIONS = 200
# But mypy fails the build:
# error: Cannot assign to final name "MAX_CONNECTIONS"On a class, Final also blocks subclasses from overriding the attribute — useful for configuration values in framework base classes.
When should you use enum instead of a constant?
Use Enum when the values form a closed set with semantics — HTTP methods, days of the week, connection states. Enum members are singletons, comparable by identity, iterable, and play nicely with Python 3.10's structural pattern matching for exhaustiveness.
from enum import Enum, IntEnum, StrEnum, auto
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
class HttpStatus(IntEnum):
OK = 200
NOT_FOUND = 404
SERVER_ERROR = 500
# Python 3.11+ — values are real strings, comparable with str
class LogLevel(StrEnum):
DEBUG = "debug"
INFO = "info"
WARN = "warn"
ERROR = "error"
# Exhaustive matching with match/case (Python 3.10+)
def handle(status: HttpStatus) -> str:
match status:
case HttpStatus.OK: return "Success"
case HttpStatus.NOT_FOUND: return "Not found"
case HttpStatus.SERVER_ERROR: return "Boom"Pick the variant by use case: Enum for opaque tokens, IntEnum when values need to participate in arithmetic or compare with plain ints, StrEnum for string-comparable values (Python 3.11+), IntFlag for bitmask-style sets.
Which immutable collection types should you reach for?
Lists and dicts are mutable. For constant collections, prefer the immutable counterparts so accidental modification raises immediately:
- Use
tupleinstead oflist. - Use
frozensetinstead ofset. - Wrap dicts with
types.MappingProxyTypefor read-only views (Python 3.3+, 2012). - Use
@dataclass(frozen=True)orNamedTuplefor constant records with named fields.
from types import MappingProxyType
from dataclasses import dataclass
ALLOWED_EXTENSIONS = ("py", "pyi", "pyx")
RESERVED_WORDS = frozenset({"if", "else", "while", "for", "def", "class"})
_CONFIG = {"debug": False, "log_level": "info"}
CONFIG = MappingProxyType(_CONFIG)
# CONFIG["debug"] = True
# → TypeError: 'mappingproxy' object does not support item assignment
@dataclass(frozen=True)
class Limits:
max_upload_mb: int
max_concurrency: int
LIMITS = Limits(max_upload_mb=50, max_concurrency=100)
# LIMITS.max_upload_mb = 100
# → dataclasses.FrozenInstanceErrorWhich built-in constants should you know?
Python's standard library spreads constants across math, sys, string, and a handful of built-ins. The most useful in everyday code:
import math, sys, string
# Math
math.pi # 3.141592653589793
math.e # 2.718281828459045
math.tau # 6.283185307179586 (2*pi — preferred since PEP 628)
math.inf # Positive infinity
math.nan # Not a Number — never compare with ==
# System / interpreter
sys.maxsize # Largest list index (2**63 - 1 on 64-bit)
sys.float_info # Float limits as a named tuple
sys.version_info # (major, minor, micro, releaselevel, serial)
# String
string.ascii_lowercase # 'abcdefghijklmnopqrstuvwxyz'
string.digits # '0123456789'
string.punctuation # all ASCII punctuation
# Built-ins
True, False
None
... # Ellipsis — used in type stubs and slicing
__debug__ # True unless Python ran with -OPractical patterns and gotchas
- Don't trust bool() on string env vars.
bool('false')returnsTruebecause the string is non-empty. Parse explicitly. - Module-level constants run once, at import time. If a "constant" is built from
os.environor a file read, it freezes the startup state — usually what you want, occasionally a footgun. - Final on classes prevents subclassing:
@finalfromtypingblocks inheritance at the type-check layer;: Finalon a method blocks override. - StrEnum is the modern way to define string constants in Python 3.11+. Members compare equal to their string value, iterate cleanly, and serialize to JSON without a custom encoder.
For language-agnostic naming, immutability, and "magic number" rules, see constants best practices. For Python's environment variable story, see environment variables in Python.