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.

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_CONNECTIONSor 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.OKis far more descriptive than using a magic number or an index likeSTATUS[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 aTypeError, preventing any reassignment. - One-Time Initialization: Constants like
MAX_SIZEare 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 distinctvalue(like1).
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.pyfile to handle API keys, database URLs, and feature flags. - Create a type-safe logging system where log levels like
LogLevel.DEBUGandLogLevel.ERRORare defined with anEnumto prevent invalid entries. - Deploy a scientific calculator with physical constants like
PIstored 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 likeif 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_registrywith a given name and its initial status, eitherTrueorFalse. - The
is_enabled()method lets you check if a feature is active by looking up its name in the registry. It safely returnsFalseif the flag doesn't exist. - Defining constants like
DARK_MODEby callingregister()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_registrydictionary 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'sget()method, which conveniently returnsFalsefor 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.
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.
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)