PYTHON / VARIABLES AND DATA TYPES
Constants and naming conventions
Name constants and other identifiers the way PEP 8 expects, and explain why UPPER_CASE protects nothing at runtime.
What you will learn
- Write module-level constants as UPPER_SNAKE_CASE, defined once near the top of a file
- Pick the right case style: snake_case names, CapWords classes, UPPER_CASE constants
- Back a constant with an immutable value (tuple, frozenset, str) so it cannot be mutated
- Use typing.Final so a type checker flags rebinding that CPython happily allows
Understanding Constants and naming conventions
Python has no const keyword. A constant is nothing more than an ordinary module-level name that you and your readers agree not to rebind, and the agreement is spelled out by writing the name in UPPER_SNAKE_CASE. The interpreter treats MAX_RETRIES exactly like max_retries: both are entries in the module's global namespace, both can be reassigned at any time, and neither triggers a warning when you do.
PEP 8 gives each kind of name its own shape so you can tell what something is without opening its definition: snake_case for variables, functions, methods and modules; CapWords for classes; UPPER_SNAKE_CASE for constants; a single leading underscore for names that are internal to a module or class; a single trailing underscore to sidestep a keyword, as in class_ or lambda_. Names with two leading and two trailing underscores are reserved by the language, so never invent your own __name__.
The useful mental model is that these conventions carry information the interpreter does not track. Uppercase says "rebinding this is a bug", but it says nothing about the object the name points at, so a constant should normally hold an immutable value: int, str, tuple, frozenset, or an Enum member. If you run mypy or pyright, annotate with typing.Final to turn the social promise into a checked one at analysis time, while remembering that at runtime the assignment would still succeed.
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 1.5
SUPPORTED_CODES = ("en", "fr", "de")
def describe():
return f"retries={MAX_RETRIES}, timeout={DEFAULT_TIMEOUT}s, codes={len(SUPPORTED_CODES)}"
print(describe())
MAX_RETRIES = 99 # legal: uppercase is a convention, not a lock
print(describe())
try:
SUPPORTED_CODES.append("es")
except AttributeError as exc:
print("AttributeError:", exc)In Python a constant is a promise expressed in a name, not a rule enforced by the interpreter, so the name style and the immutability of the value are what actually carry the meaning.
Worked examples
typing.Final marks intent for tools, not for CPython
Shows that Final is checked by static analysers while the interpreter still allows the rebinding.
from typing import Final
TAX_RATE: Final = 0.2
def with_tax(amount: float) -> float:
return round(amount * (1 + TAX_RATE), 2)
print(with_tax(100))
TAX_RATE = 0.25 # mypy: Cannot assign to final name "TAX_RATE"
print(with_tax(100))Example explained
Line 1TAX_RATE: Final = 0.2 is a normal assignment plus an annotation; the annotation is stored, not enforced.
Line 2The rebinding to 0.25 runs without error, which is why the second call returns a different number.
Line 3with_tax reads TAX_RATE from module globals on every call, so it never sees a frozen snapshot of the value.
Line 4A type checker reports the second assignment as an error, so Final buys you a failing check, not a failing run.
One file, four naming styles
Demonstrates the PEP 8 shapes for a module constant, a class, a class-level constant, an internal attribute and a keyword-clashing parameter.
MAX_WIDTH = 80
class TextBox:
DEFAULT_FILL = "."
def __init__(self, width):
self.width = min(width, MAX_WIDTH)
self._buffer = []
def add_line(self, text):
self._buffer.append(text[: self.width])
return self
def render(self, class_=None):
tag = class_ or "plain"
return f"[{tag}] " + " | ".join(self._buffer)
box = TextBox(10).add_line("hello world!").add_line("second")
print(box.render())
print(box.render("boxed"))
print(TextBox.DEFAULT_FILL * 5)Example explained
Line 1MAX_WIDTH is a module constant in caps; TextBox is a class in CapWords, so the two are never confused at a glance.
Line 2DEFAULT_FILL lives inside the class but is still a constant, so it keeps the uppercase style and is read as TextBox.DEFAULT_FILL.
Line 3self._buffer uses one leading underscore to say "internal detail", which is a hint to readers and to tools like Sphinx, not access control.
Line 4The parameter is called class_ because class is a keyword; a trailing underscore is the standard escape hatch.
Enum for a group of related constants
Shows how Enum turns a family of constants into named members that actually refuse rebinding.
from enum import Enum
class Status(Enum):
PENDING = "pending"
ACTIVE = "active"
print(Status.ACTIVE.name, Status.ACTIVE.value)
print([member.name for member in Status])
try:
Status.ACTIVE = "nope"
except AttributeError as exc:
print("rebinding blocked:", type(exc).__name__)Example explained
Line 1Members are written in caps because each one is a constant, just grouped under a class instead of scattered across the module.
Line 2member.name gives the identifier you typed and member.value gives the payload, so the two roles stay separate.
Line 3Assigning to Status.ACTIVE raises AttributeError: Enum's metaclass rejects it, which is stronger than the plain uppercase convention.
Line 4Iterating the class yields the members in definition order, which is handy for validation lists.
Important notes
typing.Final is advice for static checkers only; CPython performs no runtime check, so a Final name can still be rebound while the program runs.
Inside a class, a name with two leading underscores such as __secret is rewritten to _ClassName__secret by name mangling; that is a real language rule, unlike the purely stylistic conventions.
Common mistakes
Assuming UPPER_CASE blocks assignment: rebinding PAGE_SIZE inside a loop runs silently, and every function that reads it changes behaviour with no error to trace back to.
Binding an uppercase name to a list or dict, then calling DEFAULT_TAGS.append(...); the object mutates for every module that imported it, so the "constant" drifts between tests.
Naming a variable after a builtin, as in list = [1, 2] or type = "admin"; the next call to list(...) or type(x) fails with TypeError: 'list' object is not callable.
Try it yourself
Change, predict, then run
Write a module that defines MIN_LENGTH = 8 and ALLOWED_SYMBOLS = frozenset("!@#$"), plus a snake_case function is_strong(password) that returns True only when the password is long enough and contains at least one allowed symbol; then try ALLOWED_SYMBOLS.add("%") and print the exception message you get.
Open the Python workspaceCheck your understanding
config.py contains LIMITS = [10, 20]. Another module does `from config import LIMITS` and then `LIMITS.append(30)`. What is true afterwards?
- The append raises AttributeError, because names written in capitals are read-only
- The importing module gets its own copy of the list, so config still sees [10, 20]
- Both modules see [10, 20, 30]; the capitals only document intent, and the list object is still mutable
- Python raises a TypeError at import time because a mutable object cannot be used as a constant
Show answer
The import binds a second name to the same list object, and nothing about an uppercase name freezes or copies that object, so the append is visible everywhere. Option 2 is tempting because `from config import LIMITS` feels like taking a value away, but only rebinding (LIMITS = []) would affect just the local name; mutating the shared list affects both modules.