How to define constants in Python

Learn how to define constants in Python. Explore different methods, tips, real-world applications, and how to debug common errors.

How to define constants in Python
Published on: 
Tue
Mar 3, 2026
Updated on: 
Wed
Mar 4, 2026
The Replit Team Logo Image
The Replit Team

Defining constants in Python is a key practice for writing reliable and maintainable code. Since Python doesn't have a true constant type, developers use conventions to signal a variable should not change.

In this article, you'll explore techniques for creating constants, from simple conventions to advanced methods. We'll also cover real-world applications and debugging advice to help you write more robust code.

Using uppercase variable names

PI = 3.14159
MAX_CONNECTIONS = 100
DATABASE_URL = "postgresql://user:pass@localhost/db"

print(f"PI: {PI}, Max connections: {MAX_CONNECTIONS}")--OUTPUT--PI: 3.14159, Max connections: 100

The simplest and most common method for defining constants is a naming convention. By writing variable names in all uppercase, like PI and DATABASE_URL, you signal to other developers that these values are not meant to be changed. It's a widely accepted practice that improves code clarity.

While the Python interpreter won't stop you from reassigning an uppercase variable, this convention helps prevent accidental changes and makes the code's intent clear. It's especially useful for:

  • Configuration values: Such as MAX_CONNECTIONS or API keys.
  • Mathematical constants: Like PI.

Standard approaches to defining constants

Moving beyond simple conventions, you can also define constants using more structured approaches that help organize and protect your values from being accidentally changed.

Creating a dedicated constants module

# In constants.py
APP_NAME = "MyApp"
VERSION = "1.0.0"
DEBUG_MODE = False

# In main.py
import constants
print(f"Running {constants.APP_NAME} v{constants.VERSION}")--OUTPUT--Running MyApp v1.0.0

A great way to organize your constants is by grouping them in a dedicated module, often a file named constants.py. This approach centralizes all your application's fixed values. You can then import this module wherever you need to access them, as shown with import constants.

  • Centralization: All your constants live in one predictable place.
  • Easy Updates: You only need to change a value in one file to update it everywhere.
  • Clean Code: It keeps your main application logic free from scattered configuration values.

Using a class with class variables

class Config:
   API_KEY = "12345abcde"
   TIMEOUT = 30
   RETRY_ATTEMPTS = 3

print(f"Timeout: {Config.TIMEOUT}s, Retries: {Config.RETRY_ATTEMPTS}")--OUTPUT--Timeout: 30s, Retries: 3

You can also define constants within a class, which groups them under a single namespace. In this example, API_KEY and TIMEOUT are class variables of the Config class. You can access them directly using dot notation, like Config.TIMEOUT—you don't even need to create an instance of the class. This method offers a few key benefits:

  • Organization: It bundles related constants, such as all API settings, into one logical container.
  • Clarity: It makes your code more readable by showing where a value comes from, for example, Config.RETRY_ATTEMPTS.

Using namedtuple for grouped constants

from collections import namedtuple

HttpStatus = namedtuple('HttpStatus', ['OK', 'NOT_FOUND', 'SERVER_ERROR'])
STATUS = HttpStatus(200, 404, 500)

print(f"Success: {STATUS.OK}, Not Found: {STATUS.NOT_FOUND}")--OUTPUT--Success: 200, Not Found: 404

A namedtuple from the collections module offers a memory-efficient way to group related constants. It lets you create simple objects with named fields, so you can access values like STATUS.OK instead of using an index. This makes your code more readable and self-documenting.

  • Immutability: Because namedtuples are tuples, their values can't be changed after they're set, which is ideal for constants.
  • Clarity: Accessing STATUS.OK is far more descriptive than using a magic number or an index like STATUS[0].

Advanced techniques for immutable constants

If the standard approaches feel more like suggestions, you can use advanced techniques to truly enforce immutability and prevent accidental changes.

Using property decorators for read-only constants

class AppConstants:
   @property
   def API_URL(self):
       return "https://api.example.com/v1"
   
app = AppConstants()
print(app.API_URL)
# app.API_URL = "new_url"  # This would raise AttributeError--OUTPUT--https://api.example.com/v1

The @property decorator is a clever way to enforce immutability. It transforms a method into a read-only attribute, so you can access a value like app.API_URL without parentheses, just as you would with a regular variable.

  • Prevents changes: Since there's no corresponding setter method, any attempt to reassign the property will raise an AttributeError. This makes it truly read-only.
  • Clean access: It provides a clean, attribute-style access to a value that might be computed or, in this case, simply protected from modification.

Creating a custom constants class with attribute protection

class Constants:
   def __init__(self):
       self.MAX_SIZE = 1024
       self.DEFAULT_ENCODING = "utf-8"
       
   def __setattr__(self, name, value):
       if hasattr(self, name):
           raise TypeError(f"Cannot reassign constant {name}")
       self.__dict__[name] = value

CONST = Constants()
print(CONST.MAX_SIZE, CONST.DEFAULT_ENCODING)--OUTPUT--1024 utf-8

For more robust protection, you can create a custom class that makes its attributes immutable. This technique leverages the special __setattr__ method, which Python automatically calls every time you try to assign a value to an attribute.

  • Attribute Guarding: The custom __setattr__ logic first checks if an attribute already exists. If it does, it raises a TypeError, preventing any reassignment.
  • One-Time Initialization: Constants like MAX_SIZE are set once within the __init__ method. After that, they are effectively locked and cannot be changed.

Using the enum module for type-safe constants

from enum import Enum, auto

class LogLevel(Enum):
   DEBUG = auto()
   INFO = auto()
   WARNING = auto()
   ERROR = auto()

print(f"Log levels: {LogLevel.DEBUG.name}, {LogLevel.ERROR.name}")
print(f"Values: {LogLevel.DEBUG.value}, {LogLevel.ERROR.value}")--OUTPUT--Log levels: DEBUG, ERROR
Values: 1, 4

The enum module is perfect for creating a group of related, unchangeable constants. The LogLevel class defines a set of symbolic names, ensuring a log level can only be one of the predefined options like LogLevel.DEBUG. This prevents bugs from using incorrect values.

  • Type-safe and immutable: Enum members are constants and can't be reassigned. This lets you check if a variable is a LogLevel, preventing comparisons with raw strings or numbers.
  • Automatic values: The auto() function assigns unique values for you, so you don't have to manage them.
  • Clear attributes: Each member has both a readable name (like 'DEBUG') and a distinct value (like 1).

Move faster with Replit

Replit is an AI-powered development platform that transforms natural language into working applications. Describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.

The techniques for defining constants we've explored can be the foundation for real-world tools. Replit Agent can turn these concepts into production applications:

  • Build a configuration manager for a web service, using a dedicated constants.py file to handle API keys, database URLs, and feature flags.
  • Create a type-safe logging system where log levels like LogLevel.DEBUG and LogLevel.ERROR are defined with an Enum to prevent invalid entries.
  • Deploy a scientific calculator with physical constants like PI stored immutably in a custom class that prevents reassignment using __setattr__.

Describe your app idea, and Replit Agent writes the code, tests it, and fixes issues automatically, all in your browser.

Common errors and challenges

Even with the best conventions, you might run into a few common pitfalls when defining constants in Python.

One of the trickiest issues is accidentally modifying mutable constants. If you define a constant as a list, like CONFIG_KEYS = ['user', 'host'], the variable itself can't be reassigned, but the list's contents can still be changed with methods like .append(). This happens because the variable holds a reference to a mutable object.

  • Use immutable types: The best defense is to use immutable data structures. Replace lists with tuples, as in CONFIG_KEYS = ('user', 'host'), since tuples cannot be altered after creation.

When you centralize constants in a dedicated module, you can sometimes create a circular import. This occurs when your constants.py file imports another module that, in turn, needs to import constants.py. Python can't resolve this loop and will raise an ImportError, preventing your application from starting.

  • Restructure your code: The simplest fix is to ensure your constants module doesn't depend on other parts of your application. Keep it self-contained with static values.

Since Python is dynamically typed, there's no guarantee that a constant will always hold a value of the correct type. A number might get replaced with a string, leading to unexpected TypeError exceptions down the line. This is a common problem when values are loaded from external files.

  • Verify with isinstance(): Before using a constant in a critical operation, you can add a check like if isinstance(TIMEOUT, int): to confirm its type. This adds a layer of safety and makes debugging easier.

Avoiding accidental modification of mutable constants

Even when a variable is named like a constant, if it holds a mutable object like a dictionary, its contents can be changed. This can silently introduce bugs by altering configuration settings or other critical data during runtime.

The following code shows how a DATABASE_CONFIG dictionary, intended as a constant, is easily modified. A simple assignment changes the port from 5432 to 3306, which could break an application's connection to its database without any obvious error at the source.

# Constants defined as mutable objects can be modified
DATABASE_CONFIG = {
   "host": "localhost",
   "port": 5432,
   "username": "admin"
}

# Later in the code
DATABASE_CONFIG["port"] = 3306  # This modifies the "constant"
print(f"Database now uses port: {DATABASE_CONFIG['port']}")  # 3306

Because the DATABASE_CONFIG dictionary is mutable, the assignment to DATABASE_CONFIG["port"] directly alters its contents. This works because the variable only references the object. The following code shows how to prevent such changes.

# Using MappingProxyType to create a truly immutable dictionary
from types import MappingProxyType

DATABASE_CONFIG = MappingProxyType({
   "host": "localhost",
   "port": 5432,
   "username": "admin"
})

# This would raise TypeError:
# DATABASE_CONFIG["port"] = 3306

print(f"Database port: {DATABASE_CONFIG['port']}")  # Always 5432

To prevent modifications, wrap your dictionary in a MappingProxyType from the types module. This gives you a read-only view of the dictionary's data. If you try to change a value, Python will raise a TypeError, effectively making your configuration immutable. It’s a powerful way to protect critical data like database settings or feature flags from being accidentally altered during runtime.

Debugging issues with import statements in constants modules

A circular import is a common headache that occurs when your constants.py module tries to import another module that, in turn, imports it. This creates a dependency loop Python can't resolve, triggering an ImportError. The code below shows this deadlock in action.

# constants.py
import app_config  # Circular import!
APP_NAME = "MyApp"
DEBUG = app_config.is_debug_mode()

# app_config.py
import constants  # Circular import!
def is_debug_mode():
   return constants.APP_NAME == "MyApp-Debug"

Here, constants.py needs app_config.py to load, but app_config.py needs constants.py first. This creates a deadlock where neither module can finish importing the other, triggering an ImportError. The following code demonstrates how to resolve this dependency loop.

# constants.py
APP_NAME = "MyApp"
DEBUG = False  # Default value

# app_config.py
import constants
def initialize_config():
   # Modify constants after import to avoid circular dependency
   if constants.APP_NAME == "MyApp-Debug":
       constants.DEBUG = True

The solution resolves the circular import by removing the import app_config from constants.py. Instead, the constants module sets a default value, like DEBUG = False. The app_config.py module can then safely import constants and modify its values at runtime through a function. This breaks the dependency loop by delaying the configuration. You'll often see this issue when a constants file needs dynamic values from another module that also depends on it.

Handling type consistency with isinstance() checks

Since Python is dynamically typed, a constant can accidentally hold the wrong type of value, like a string instead of a number. This often leads to a TypeError when you try to use it. Without an isinstance() check, this can be tricky to debug. The code below shows how a simple type mismatch breaks a program when a string is passed to the range() function.

# Constants with incorrect types
MAX_RETRIES = "5"  # String instead of integer

def retry_operation():
   for attempt in range(MAX_RETRIES):  # TypeError: 'str' object cannot be interpreted as an integer
       print(f"Attempt {attempt + 1}")

The range() function requires an integer, but the MAX_RETRIES constant is a string. This mismatch causes a TypeError because Python can't interpret the string as a number for the loop. The code below shows how to prevent this.

# Adding type validation for constants
MAX_RETRIES = 5

def retry_operation():
   if not isinstance(MAX_RETRIES, int):
       raise TypeError("MAX_RETRIES must be an integer")
   
   for attempt in range(MAX_RETRIES):
       print(f"Attempt {attempt + 1}")

The solution is to validate the constant's type using isinstance(). The corrected code checks if MAX_RETRIES is an integer before the loop begins. If it isn't, the code raises a TypeError with a clear error message. This proactive check is crucial when loading values from external sources like configuration files, as they often default to strings. It makes your code more robust and simplifies debugging by catching type mismatches early.

Real-world applications

By applying these techniques and avoiding common pitfalls, you can build robust tools like configuration parsers and feature flag systems.

Building a simple configuration file parser with constants

You can use constants like CONFIG_SEPARATOR and COMMENT_SYMBOL to define the rules for a simple parser, allowing you to reliably extract settings from a configuration file.

CONFIG_SEPARATOR = "="
COMMENT_SYMBOL = "#"

def parse_config_line(line):
   if COMMENT_SYMBOL in line:
       line = line[:line.index(COMMENT_SYMBOL)]
   
   if CONFIG_SEPARATOR in line:
       key, value = line.split(CONFIG_SEPARATOR, 1)
       return key.strip(), value.strip()
   return None, None

config_line = "database_url = sqlite:///app.db  # Local database"
key, value = parse_config_line(config_line)
print(f"{key}: {value}")

The parse_config_line function is designed to read a single line from a configuration file. It works in two main steps:

  • First, it scans for a comment, marked by a #, and strips it away to ignore anything that isn't part of the configuration.
  • Next, it splits the cleaned line at the = separator to extract the key and value pair.

This process allows you to reliably pull settings from each line while cleanly handling comments. If a line doesn't contain a setting, the function returns None.

Implementing feature flags using constants and a registry

You can build a powerful feature flag system by using a registry class to manage constants, which lets you toggle application behavior on the fly.

This approach uses a central FeatureFlags class to act as a single source of truth. Each feature, like DARK_MODE, is registered as a constant, making it easy to check its status from anywhere in your application.

The FeatureFlags class provides a clean and centralized way to manage your application's features. It works by using a dictionary called _registry to keep track of each flag's state.

  • The register() class method adds a new feature to the _registry with a given name and its initial status, either True or False.
  • The is_enabled() method lets you check if a feature is active by looking up its name in the registry. It safely returns False if the flag doesn't exist.
  • Defining constants like DARK_MODE by calling register() ensures that all features are centrally declared and managed from a single place.

class FeatureFlags:
   _registry = {}
   
   @classmethod
   def register(cls, name, enabled=False):
       cls._registry[name] = enabled
       return enabled
   
   @classmethod
   def is_enabled(cls, name):
       return cls._registry.get(name, False)

# Define feature constants
DARK_MODE = FeatureFlags.register("DARK_MODE", True)
BETA_FEATURES = FeatureFlags.register("BETA_FEATURES", False)

print(f"Dark mode: {FeatureFlags.is_enabled('DARK_MODE')}")
print(f"Beta features: {FeatureFlags.is_enabled('BETA_FEATURES')}")

This code creates a simple feature flag system using a class as a central manager. Because the methods use the @classmethod decorator, you can call them directly on the FeatureFlags class without creating an object instance.

  • The register() method populates a shared _registry dictionary with feature names and their boolean state.
  • The is_enabled() method then provides a safe way to check a flag's status. It uses the dictionary's get() method, which conveniently returns False for any unregistered or misspelled flags, preventing potential errors.

Get started with Replit

Turn these concepts into a real tool with Replit Agent. Try prompts like: "Build a config parser with immutable settings" or "Create a scientific calculator with a dedicated constants module for physical values."

Replit Agent writes the code, tests for errors, and deploys your application. Start building with Replit.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started for free

Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.