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

Python lacks a formal syntax for constants. Instead, developers use naming conventions to indicate a value should not change. This practice improves code clarity and helps prevent accidental modifications.
In this article, you’ll explore techniques to define and manage constants. We’ll cover practical tips, real-world applications, and debugging advice to help you write robust and maintainable Python 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 most common way to declare constants in Python is by using all-uppercase variable names. This is a widely accepted convention, not a strict rule enforced by the language itself. Python doesn't actually prevent you from reassigning these variables.
In the example, variables like PI, MAX_CONNECTIONS, and DATABASE_URL are written in uppercase. This signals to anyone reading the code that these values are intended to be constant and shouldn't be changed. Following this convention makes your code more predictable and easier for others to understand.
Standard approaches to defining constants
While the uppercase convention works for simple scripts, larger applications often benefit from more structured approaches to defining and managing constants.
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
For larger projects, a common strategy is to group all your constants into a dedicated file, such as constants.py. You can then import this module into other parts of your application, like main.py, and access values using dot notation (e.g., constants.APP_NAME). This approach offers a few key advantages:
- It centralizes all configuration values, making them easy to find and update.
- It keeps your main logic clean by separating it from static data.
- It improves clarity, as the
constants.prefix shows exactly where a value comes from.
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 group constants by defining them as class variables inside a dedicated class, like Config. This method neatly bundles related settings, such as API credentials or retry logic. Since these are class variables, you don't need to create an object—you can access them directly using dot notation, like Config.TIMEOUT.
- This creates a distinct namespace, which helps avoid naming collisions.
- It keeps your configuration organized and easy to manage.
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
For constants that belong together, a namedtuple from the collections module offers a clean and readable solution. It lets you create a simple, immutable object where values can be accessed by name instead of by index, making your code more self-documenting.
- In the example,
STATUS.OKis much clearer than a magic number like200or an index likestatus[0]. - Because
namedtuplecreates a tuple, its values are immutable, which reinforces their role as constants.
Advanced techniques for immutable constants
While the standard approaches rely on convention, you can also use more advanced techniques to create constants that are truly immutable and protected from 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 transforms a method into a read-only attribute, offering a more robust way to define constants. In this example, you can access app.API_URL just like a regular variable, but the value is returned by the underlying method. Since there’s no corresponding setter method defined, the attribute is effectively read-only.
- This provides true immutability—any attempt to reassign the value, like
app.API_URL = "new_url", will raise anAttributeErrorand stop the program.
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
This approach involves creating a custom class that intercepts attribute assignments. By overriding the special __setattr__ method, you can control how values are set. This gives you the power to prevent changes after a constant has been defined, making your code more robust.
- The
__setattr__method checks if an attribute already exists usinghasattr(). - If it does, any attempt to reassign it will raise a
TypeError. - This effectively makes all attributes of the
CONSTobject immutable once they're set in__init__.
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 ideal for creating a group of related, immutable constants. It provides type safety, which prevents you from accidentally comparing a log level with an unrelated number. The LogLevel class, for instance, defines a fixed set of states.
- Using
auto()assigns a unique integer value to each member automatically. - Each member, like
LogLevel.DEBUG, has both a readable.name('DEBUG') and an underlying.value(1). - This makes your code more robust by ensuring you're using a valid
LogLevel, not just a magic number.
Move faster with Replit
Replit is an AI-powered development platform where you can start coding Python instantly. It comes with all Python dependencies pre-installed, so you can skip setup and focus on building.
This allows you to move from learning individual techniques to building complete applications with Agent 4. Describe the app you want to build, and the Agent will write the code, connect to databases and APIs, and manage deployment.
- A configuration utility that centralizes API keys and settings in a
Configclass for a web app. - A monitoring script that uses an
Enumto track and report service statuses likeONLINEorMAINTENANCE. - A scientific calculator that pulls immutable values like
PIfrom a dedicated constants module.
Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.
Common errors and challenges
While defining constants seems straightforward, a few common challenges can trip up even experienced developers.
One of the most common mistakes is using a mutable object, like a list or dictionary, as a constant. While you can't reassign the variable name itself by convention, you can still change the object's contents. For example, if you define ALLOWED_USERS = ['admin', 'guest'], another part of the code could accidentally call ALLOWED_USERS.append('new_user'), modifying your "constant" without warning.
This can lead to unpredictable behavior and bugs that are difficult to trace. To prevent this, you should use immutable data types for your constants whenever possible.
- Instead of a list, use a tuple:
ALLOWED_USERS = ('admin', 'guest'). Tuples don't have methods likeappend()orremove(), so their contents can't be changed after creation. - For sets of unique items, use a
frozenset, which is the immutable version of a set.
When you centralize constants in a dedicated module like constants.py, you risk creating circular dependencies. This happens when your constants module imports from another module that, in turn, imports from your constants module. For example, if constants.py imports from utils.py to compute a value, but utils.py also needs a constant from constants.py, Python gets stuck.
This circular logic will raise an ImportError because one module can't finish loading before the other one needs it. The best solution is to refactor your code to break the cycle—this might mean moving the shared dependency to a third, more basic module or rethinking your project structure so that constants don't need to import from higher-level application code.
When you use constants, especially with enums, you often need to ensure that a variable holds one of the valid, predefined values. Simply comparing values isn't always enough and can lead to subtle bugs. For instance, if your LogLevel.ERROR has a value of 4, a function might mistakenly accept the integer 4 from another source, even though it's not a true LogLevel member.
A more robust approach is to use the built-in isinstance() function to verify the type. A check like isinstance(level, LogLevel) confirms that the level variable is an actual instance of the LogLevel enum, not just a number that happens to match. This practice enforces type safety, making your functions more predictable and preventing them from accepting incorrect data types.
Avoiding accidental modification of mutable constants
Using a dictionary for constants like DATABASE_CONFIG is a common pitfall. Because dictionaries are mutable, their values can be changed accidentally, which can introduce subtle bugs that are difficult to trace. The following example shows how easily this can happen.
# 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
The code directly reassigns the port key's value. Since dictionaries are mutable, this change happens silently and can cause bugs. The following example shows a more robust way to define such configuration data.
# 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 accidental changes, wrap your dictionary in a MappingProxyType. This creates a read-only view of your dictionary, making it immutable. Any attempt to modify it will raise a TypeError, stopping the program. This technique is perfect for protecting shared configuration data, like database settings or API keys, ensuring they remain constant throughout your application's runtime. It’s a simple way to add a layer of safety.
Debugging issues with import statements in constants modules
Circular imports are a common headache when using a dedicated constants module. This error occurs when two files, like constants.py and app_config.py, try to import each other simultaneously, creating a loop that Python can't resolve. The following code demonstrates this problem.
# 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"
The problem is that constants.py imports app_config to use is_debug_mode(), while app_config imports constants to check APP_NAME. Neither module can finish loading because each is waiting for the other. The code below demonstrates a fix.
# 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 fix is to break the import cycle. Instead of importing from app_config, the constants.py module now sets a default value for DEBUG. This allows app_config.py to import constants without issue. A separate function, like initialize_config(), can then be called after the initial imports to modify the constant's value based on runtime logic.
This pattern is useful when your constants depend on other application logic. It defers the configuration step until after all modules are loaded, neatly avoiding an ImportError.
Handling type consistency with isinstance() checks
Even when a constant's value looks correct, its data type might be wrong. A number defined as a string, for instance, will cause a TypeError during mathematical operations. This subtle mistake can break your code in unexpected ways. The following code shows just how easily this can happen when a constant is used in a range() function that expects an integer.
# 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 TypeError is raised because the range() function expects an integer but receives the string "5". The following example shows how to add a check to ensure the constant's type is correct before it's used.
# 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 fix is to validate the constant's type before it's used. An isinstance(MAX_RETRIES, int) check confirms that MAX_RETRIES is an integer, not a string, preventing a TypeError. This makes your function more reliable by catching type mismatches early.
This kind of check is crucial when constants come from configuration files or user input, where the data type isn't always guaranteed. It ensures your code fails predictably with a clear error message.
Real-world applications
Now that you've seen how to handle common errors, you can apply these techniques to build tools like configuration parsers and feature flags.
Building a simple configuration file parser with constants
You can use constants like CONFIG_SEPARATOR and COMMENT_SYMBOL to define key characters, making your configuration file parser cleaner and easier to maintain.
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}")
This code defines a function, parse_config_line, that processes a single line from a configuration file. It’s designed to extract a key-value pair while ignoring comments, making it useful for reading settings from simple text files.
- First, it finds the
COMMENT_SYMBOL(#) and discards any part of the line that follows it. - Next, it splits the cleaned line at the
CONFIG_SEPARATOR(=) to separate the key from the value. - Finally, it uses
strip()to remove extra whitespace from both parts before returning them.
Implementing feature flags using constants and a registry
This approach uses a FeatureFlags class to create a central registry, giving you a structured way to define and manage features that can be toggled on or off.
The class holds a private dictionary called _registry to store the status of each feature. The register method adds a new feature flag by name and sets its initial state, while the is_enabled method provides a safe way to check if a feature is active. This prevents you from having to scatter boolean checks throughout your codebase.
- The
registerclass method adds a feature name and its boolean state, such asTrueorFalse, to the_registry. - Constants like
DARK_MODEare defined by callingFeatureFlags.register("DARK_MODE", True), which both registers the feature and assigns its state to the constant. - You can then check a feature's status anywhere in your app with
FeatureFlags.is_enabled('DARK_MODE'), which returns the current state from the registry.
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 system for managing feature flags. The FeatureFlags class uses two class methods, register and is_enabled, to control features across your application from a single place.
- The
registermethod defines a new feature, likeDARK_MODE, and stores its on/off state in a shared dictionary. - You can then use
is_enabledto check if a feature is active anywhere in your code.
This pattern centralizes your feature toggles, so it's easy to update them without changing logic scattered throughout the project.
Get started with Replit
Now, turn these concepts into a working tool. Describe what you want to build to Replit Agent, like “a currency converter using a constants module” or “a settings utility with immutable config for a Flask app.”
The Agent will write the code, test for errors, and deploy your application for you. Start building with Replit.
Describe what you want to build, and Replit Agent writes the code, handles the infrastructure, and ships it live. Go from idea to real product, all in your browser.
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.

.png)
.png)
.png)