Constants #

Python has no special keyword for defining constants like const in JavaScript or final in Java. There’s no built-in mechanism preventing a value from being changed after it’s set. What Python does have is a convention: names written entirely in capital letters are treated as a signal to other developers that this value should not be changed. That convention sounds weak, but in practice it’s remarkably effective because Python code leans heavily on community agreement. This article covers how to use constants properly in Python: from the basic convention, to hardening them with Final and Enum, to patterns for organizing constants in larger projects.

The UPPER_SNAKE_CASE Convention #

The most common way to define constants in Python is using all-caps names with words separated by underscores. It’s a PEP 8 convention every Python developer understands.

# Mathematical and physical constants
PI = 3.141592653589793
GRAVITY = 9.80665          # m/s²
SPEED_OF_LIGHT = 299_792_458  # m/s

# Application config constants
MAX_CONNECTIONS = 100
TIMEOUT_SECONDS = 30
PAGE_SIZE = 20

# String constants
BASE_URL = "https://api.example.com/v1"
API_VERSION = "v1.2.0"
APP_NAME = "SistemKasir"

# Path constants
UPLOAD_DIR = "/var/uploads"
LOG_DIR = "/var/log/app"

Large numbers can be written with underscores as thousands separators for readability:

# ANTI-PATTERN: big number without separators — hard to read
CREDIT_LIMIT = 50000000
INDONESIA_POPULATION = 270000000

# CORRECT: use underscores as thousands separators
CREDIT_LIMIT = 50_000_000
INDONESIA_POPULATION = 270_000_000

Why Constants Matter: Magic Numbers #

The biggest problem constants solve is the magic number — a number or string literal that appears out of nowhere in code with no explanation of what it means.

# ANTI-PATTERN: magic numbers — what do 0.11, 18, and 3 mean?
def compute_total(price, age):
    tax = price * 0.11
    if age < 18:
        return -1
    if len(cart) > 3:
        apply_discount()
    return price + tax

# Questions the code above leaves unanswered:
# - Is 0.11 VAT? what rate? when does it change?
# - Is 18 the legal age limit? for what?
# - Is 3 a minimum item count? a maximum?
# - What does -1 mean? an error? invalid?
# CORRECT: replace magic numbers with named constants
VAT_RATE = 0.11          # 11% VAT in effect since April 2022
ADULT_AGE_LIMIT = 18     # per the Child Protection Act
MIN_ITEMS_FOR_DISCOUNT = 3       # discount applies from the 4th item
NOT_ADULT_CODE = -1      # return code if the buyer is underage

def compute_total(price, age):
    tax = price * VAT_RATE
    if age < ADULT_AGE_LIMIT:
        return NOT_ADULT_CODE
    if len(cart) > MIN_ITEMS_FOR_DISCOUNT:
        apply_discount()
    return price + tax

The immediate payoff: if the VAT rate changes, you only edit one line (VAT_RATE = 0.12), instead of hunting down every occurrence of 0.11 across the codebase.


Final — Annotating Constants That Must Not Be Reassigned #

Since Python 3.8, the typing module provides Final, which can be used as a type annotation and a signal to linters that a variable must not be reassigned. Python still doesn’t prevent the change at runtime, but tools like mypy will flag violations as errors.

from typing import Final

# Declaring constants with Final
PI: Final = 3.141592653589793
MAX_RETRY: Final[int] = 3
BASE_URL: Final[str] = "https://api.example.com"

# mypy will flag these as errors:
PI = 3.14          # error: Cannot assign to final name "PI"
MAX_RETRY = 5      # error: Cannot assign to final name "MAX_RETRY"

To clarify the difference between how Python and compiled languages (like Java) handle constants, compare the variable-checking flows below:

flowchart TD
    subgraph Python ["Python (Final Annotation)"]
        py_code["Code: PI: Final = 3.14"] --> py_lint["Linter (mypy)"]
        py_lint -->|Finds Re-assignment| py_err["Reports Linter Warning/Error"]
        py_code --> py_pvm["PVM Execution (Runtime)"]
        py_pvm -->|Changes the Value| py_run["Value Changes Without Runtime Error"]
    end

    subgraph Java ["Java (final Keyword)"]
        java_code["Code: final double PI = 3.14;"] --> java_comp["Compiler (javac)"]
        java_comp -->|Finds Re-assignment| java_err["Compilation Fails (Compile Error)"]
    end

The diagram above shows that Python separates type-checking responsibilities. Your linter or code editor will warn you when a Final variable is reassigned, but when the application runs (runtime), the Python Virtual Machine (PVM) won’t stop program execution.

Final can also be used inside classes for attributes that subclasses must not override:

from typing import Final

class DatabaseConfig:
    HOST: Final = "localhost"
    PORT: Final[int] = 5432
    DB_NAME: Final = "production"

    def __init__(self):
        self.username = "admin"    # regular attribute, can change
        self.password = "secret"   # regular attribute, can change
Final works as a static annotation — only recognized by linters and IDEs, not by Python at runtime. To truly prevent value changes at runtime, use Enum or a property with a getter only (no setter).

Enum — Grouped Constants #

Enum (Enumeration) is the best way to group related constants. Instead of defining many separate constants, you wrap them all in a single Enum class.

Basic Enum #

from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    COMPLETED = "completed"
    CANCELLED = "cancelled"

# Usage
order_status = OrderStatus.PENDING
print(order_status)        # → OrderStatus.PENDING
print(order_status.value)  # → pending
print(order_status.name)   # → PENDING

# Comparison — always compare against Enum members, not raw strings
if order_status == OrderStatus.PENDING:
    print("Order not processed yet")

# Iterate all members
for status in OrderStatus:
    print(status.name, "→", status.value)

IntEnum for Integer Constants #

from enum import IntEnum

class TicketPriority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

# IntEnum can be compared directly with integers
ticket_prio = TicketPriority.HIGH
print(ticket_prio > 2)        # → True
print(ticket_prio == 3)       # → True
print(int(ticket_prio))       # → 3

# Useful for sorting
ticket_list = [
    TicketPriority.LOW,
    TicketPriority.CRITICAL,
    TicketPriority.MEDIUM,
]
print(sorted(ticket_list))
# → [<TicketPriority.LOW: 1>, <TicketPriority.MEDIUM: 2>, <TicketPriority.CRITICAL: 4>]

Why Enum Beats Plain String Constants #

# ANTI-PATTERN: separate string constants — easy to typo, no autocomplete
STATUS_PENDING = "pending"
STATUS_PROCESSING = "processing"
STATUS_COMPLETED = "completed"

def update_status(order_id, status):
    if status == "pendng":      # ← typo! no error, but wrong logic
        send_pending_notification()

# CORRECT: Enum prevents invalid values
from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    COMPLETED = "completed"

def update_status(order_id, status: OrderStatus):
    if status == OrderStatus.PENDING:
        send_pending_notification()

# No typos possible — the IDE provides autocomplete
update_status(1, OrderStatus.PENDING)   # ✓
update_status(1, "pendng")              # ✗ mypy will flag this

auto() for Automatic Values #

from enum import Enum, auto

class Day(Enum):
    MONDAY = auto()    # → 1
    TUESDAY = auto()   # → 2
    WEDNESDAY = auto() # → 3
    THURSDAY = auto()  # → 4
    FRIDAY = auto()    # → 5
    SATURDAY = auto()  # → 6
    SUNDAY = auto()    # → 7

print(Day.MONDAY.value)   # → 1
print(Day.FRIDAY.value)   # → 5

Centralized Constants Module #

For larger projects, separate all constants into a single module (or several modules by category). This makes config changes easier to track and keeps values from being scattered across the codebase.

project/
  ├── constants/
  │   ├── __init__.py
  │   ├── api.py        ← API-related constants
  │   ├── database.py   ← database-related constants
  │   └── business.py   ← business rules and logic
  ├── main.py
  └── utils.py

constants/api.py #

# constants/api.py
from typing import Final

BASE_URL: Final = "https://api.example.com/v1"
TIMEOUT_SECONDS: Final[int] = 30
MAX_RETRY: Final[int] = 3
PAGE_SIZE: Final[int] = 20

DEFAULT_HEADER: Final = {
    "Content-Type": "application/json",
    "Accept": "application/json",
}

constants/business.py #

# constants/business.py
from typing import Final
from enum import Enum

# Limits and caps
MIN_BALANCE: Final[int] = 10_000
MAX_DAILY_TRANSFER: Final[int] = 25_000_000
DEFAULT_CREDIT_LIMIT: Final[int] = 5_000_000

# Rates
VAT_RATE: Final[float] = 0.11
MONTHLY_INTEREST_RATE: Final[float] = 0.015
TRANSFER_FEE: Final[int] = 6_500

# Transaction status
class TransactionStatus(Enum):
    PENDING = "pending"
    SUCCESS = "success"
    FAILED = "failed"
    CANCELLED = "cancelled"
    EXPIRED = "expired"

How to Import #

# main.py
from constants.api import BASE_URL, TIMEOUT_SECONDS, MAX_RETRY
from constants.business import VAT_RATE, TransactionStatus, MIN_BALANCE

def process_payment(amount: int) -> TransactionStatus:
    if amount < MIN_BALANCE:
        return TransactionStatus.FAILED

    tax = amount * VAT_RATE
    total = amount + tax

    # ... process the payment ...
    return TransactionStatus.SUCCESS

Constants Protected with property #

If you truly want constants that can’t change at runtime (not just by convention), use a property on a class without a setter:

class AppConfig:
    """Application configuration that can't be changed after initialization."""

    def __init__(self):
        self._version = "2.1.0"
        self._name = "SistemKasir"
        self._debug_mode = False

    @property
    def version(self) -> str:
        return self._version

    @property
    def name(self) -> str:
        return self._name

    @property
    def debug_mode(self) -> bool:
        return self._debug_mode

# Usage
config = AppConfig()
print(config.version)       # → 2.1.0
print(config.name)          # → SistemKasir

config.version = "3.0.0"    # AttributeError: can't set attribute

This approach suits configuration whose values are determined at startup (e.g. read from environment variables) and must not change while the application runs.


Python’s Built-in Constants #

Python itself defines several built-in constants worth knowing:

# Python's special built-in values
print(True)     # → True  (bool)
print(False)    # → False (bool)
print(None)     # → None  (NoneType) — represents "no value"

# Mathematical constants from the math module
import math
print(math.pi)   # → 3.141592653589793
print(math.e)    # → 2.718281828459045
print(math.inf)  # → inf  (infinity)
print(math.nan)  # → nan  (Not a Number)

# Check special values
print(math.isinf(math.inf))  # → True
print(math.isnan(math.nan))  # → True

# System constants from the sys module
import sys
print(sys.maxsize)    # → 9223372036854775807 (maximum integer on this platform)
print(sys.version)    # → the running Python version

Summary #

  • The UPPER_SNAKE_CASE convention is the standard way to define constants in Python — understood by every Python developer and needs no extra imports.
  • Eliminate magic numbers — every number or string literal whose meaning isn’t clear from context should be replaced with a named constant. This makes code easier to read and easier to change.
  • Use Final from typing to signal to linters that a variable must not be reassigned — mypy flags violations as errors.
  • Use Enum for related constants in a group — prevents typos, enables IDE autocomplete, and makes code more expressive.
  • IntEnum is a good fit when constants need numeric comparison or sorting.
  • Separate constants into their own module (constants/) in larger projects — value changes only need to happen in one place.
  • A property without a setter is the only way to truly prevent value changes at runtime — suitable for configuration read at startup.
  • Use underscores as thousands separators in large numeric constants (50_000_000) for readability.

← Previous: Variables   Next: Data Types →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact