Constants & Configuration Management Guide¶
Professional Handling of Magic Numbers and Strings in Python Projects
Table of Contents¶
- What Are Magic Numbers/Strings?
- Constants Hierarchy
- Project Organization
- Application Constants
- Enumerations (Enums)
- Configuration Management
- User-Facing Strings (i18n)
- Environment-Specific Values
- PySide6-Specific Patterns
- Best Practices
- Refactoring Strategy
What Are Magic Numbers/Strings?¶
The Problem¶
# BAD - Magic numbers and strings everywhere
def calculate_price(quantity):
if quantity > 100:
return quantity * 9.99 * 0.9 # What does 0.9 mean?
return quantity * 9.99
def send_notification(user):
if user.status == "premium": # String duplicated everywhere
send_email(user.email, "New Feature Available")
def validate_input(value):
if len(value) < 8: # Why 8?
return False
if value == "admin": # Hardcoded admin
return True
Problems: - Meaning unclear (what is 0.9? Why 8?) - Duplication (same values in many places) - Hard to change (find and update all occurrences) - Error-prone (typo: "premum" instead of "premium") - Not testable (values cannot be easily mocked)
The Solution¶
# GOOD - Named constants
from myapp.constants import (
BULK_DISCOUNT_THRESHOLD,
BULK_DISCOUNT_RATE,
UNIT_PRICE,
MIN_PASSWORD_LENGTH,
ADMIN_USERNAME,
)
from myapp.constants import UserStatus, NotificationTemplate
def calculate_price(quantity: int) -> float:
"""Calculate price with bulk discount."""
if quantity > BULK_DISCOUNT_THRESHOLD:
return quantity * UNIT_PRICE * (1 - BULK_DISCOUNT_RATE)
return quantity * UNIT_PRICE
def send_notification(user: User) -> None:
"""Send notification based on user status."""
if user.status == UserStatus.PREMIUM:
send_email(user.email, NotificationTemplate.NEW_FEATURE)
def validate_input(value: str) -> bool:
"""Validate user input."""
if len(value) < MIN_PASSWORD_LENGTH:
return False
if value == ADMIN_USERNAME:
return True
return True
Benefits: - Self-documenting - Centrally managed - Easy to change - Type-safe (with Enums) - Testable
Constants Hierarchy¶
Overview: When to Use Which Solution?¶
+-------------------------------------------------------------+
| CONSTANTS HIERARCHY |
+-------------------------------------------------------------+
| |
| 1. HARDCODED VALUES (in code) |
| - Python built-ins (True, False, None) |
| - Obvious numbers (0, 1, -1) |
| - One-time local calculations |
| |
| 2. MODULE CONSTANTS (constants.py) |
| - Business logic constants |
| - Numeric thresholds |
| - Format strings |
| - System-wide defaults |
| |
| 3. ENUMERATIONS (enums.py) |
| - State values (state machine) |
| - Categories/types |
| - Options/choices |
| - Type-safe string alternatives |
| |
| 4. CONFIGURATION (config.py + files) |
| - Environment-dependent (dev/prod) |
| - Deployment-specific |
| - User-configurable |
| - Runtime-changeable |
| |
| 5. USER-FACING STRINGS (messages.py + i18n) |
| - UI text |
| - Error messages |
| - Notifications |
| - Translatable text |
| |
| 6. ENVIRONMENT VARIABLES (.env) |
| - Secrets (API keys, passwords) |
| - URLs (APIs, databases) |
| - Feature flags |
| - Deployment configuration |
| |
+-------------------------------------------------------------+
Project Organization¶
Recommended Structure for Large Projects¶
myproject/
├── src/
│ └── myapp/
│ ├── __init__.py
│ │
│ ├── constants/ # Konstanten-Package
│ │ ├── __init__.py # Public API exports
│ │ ├── app.py # Application constants
│ │ ├── business.py # Business logic constants
│ │ ├── ui.py # UI-related constants
│ │ ├── files.py # File paths, extensions
│ │ └── defaults.py # Default values
│ │
│ ├── enums/ # Enumerations
│ │ ├── __init__.py
│ │ ├── user.py # User-related enums
│ │ ├── status.py # Status enums
│ │ └── types.py # Type enums
│ │
│ ├── config/ # Configuration
│ │ ├── __init__.py
│ │ ├── settings.py # Settings class
│ │ ├── development.py # Dev config
│ │ ├── production.py # Prod config
│ │ └── testing.py # Test config
│ │
│ ├── messages/ # User-facing strings
│ │ ├── __init__.py
│ │ ├── errors.py # Error messages
│ │ ├── notifications.py # Notifications
│ │ ├── ui_text.py # UI labels/tooltips
│ │ └── translations/ # i18n files
│ │ ├── en_US.json
│ │ └── de_DE.json
│ │
│ ├── models/
│ ├── views/
│ └── controllers/
│
├── config/ # External config files
│ ├── app.toml
│ ├── logging.yaml
│ └── database.json
│
├── .env # Environment variables
├── .env.example # Template
└── pyproject.toml
Public API Export (constants/init.py)¶
"""
Constants package public API.
This module exports all constants used throughout the application.
Import from here, not from individual modules.
Example:
>>> from myapp.constants import MAX_RETRIES, UserStatus
>>> from myapp.constants import DEFAULT_TIMEOUT
"""
# Application constants
from myapp.constants.app import (
APP_NAME,
APP_VERSION,
APP_AUTHOR,
MAX_RETRIES,
DEFAULT_TIMEOUT,
)
# Business logic constants
from myapp.constants.business import (
BULK_DISCOUNT_THRESHOLD,
BULK_DISCOUNT_RATE,
MIN_ORDER_AMOUNT,
TAX_RATE,
)
# UI constants
from myapp.constants.ui import (
WINDOW_WIDTH,
WINDOW_HEIGHT,
ICON_SIZE,
ANIMATION_DURATION,
)
# File constants
from myapp.constants.files import (
DATA_DIR,
BACKUP_DIR,
EXPORT_DIR,
ALLOWED_EXTENSIONS,
)
# Enums
from myapp.enums import (
UserStatus,
FileType,
ProcessingState,
ConnectionType,
)
__all__ = [
# App
'APP_NAME',
'APP_VERSION',
'APP_AUTHOR',
'MAX_RETRIES',
'DEFAULT_TIMEOUT',
# Business
'BULK_DISCOUNT_THRESHOLD',
'BULK_DISCOUNT_RATE',
'MIN_ORDER_AMOUNT',
'TAX_RATE',
# UI
'WINDOW_WIDTH',
'WINDOW_HEIGHT',
'ICON_SIZE',
'ANIMATION_DURATION',
# Files
'DATA_DIR',
'BACKUP_DIR',
'EXPORT_DIR',
'ALLOWED_EXTENSIONS',
# Enums
'UserStatus',
'FileType',
'ProcessingState',
'ConnectionType',
]
Application Constants¶
constants/app.py¶
"""
Core application constants.
These are fundamental constants used throughout the application
that rarely change.
"""
from pathlib import Path
# Application metadata
APP_NAME: str = "MyApp"
APP_VERSION: str = "1.0.0"
APP_AUTHOR: str = "Your Name"
APP_ORGANIZATION: str = "Your Organization"
# Network settings
MAX_RETRIES: int = 3
DEFAULT_TIMEOUT: int = 30 # seconds
CONNECT_TIMEOUT: int = 10 # seconds
READ_TIMEOUT: int = 30 # seconds
# Rate limiting
MAX_REQUESTS_PER_MINUTE: int = 60
RATE_LIMIT_WINDOW: int = 60 # seconds
# Pagination
DEFAULT_PAGE_SIZE: int = 50
MAX_PAGE_SIZE: int = 1000
# Validation
MIN_USERNAME_LENGTH: int = 3
MAX_USERNAME_LENGTH: int = 50
MIN_PASSWORD_LENGTH: int = 8
MAX_PASSWORD_LENGTH: int = 128
# Dates and times
DATE_FORMAT: str = "%Y-%m-%d"
TIME_FORMAT: str = "%H:%M:%S"
DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
ISO_DATETIME_FORMAT: str = "%Y-%m-%dT%H:%M:%SZ"
# Encoding
DEFAULT_ENCODING: str = "utf-8"
FALLBACK_ENCODING: str = "latin-1"
constants/business.py¶
"""
Business logic constants.
Constants related to business rules and calculations.
"""
# Pricing
UNIT_PRICE: float = 9.99
BULK_DISCOUNT_THRESHOLD: int = 100
BULK_DISCOUNT_RATE: float = 0.10 # 10%
PREMIUM_DISCOUNT_RATE: float = 0.15 # 15%
# Tax rates (by region)
DEFAULT_TAX_RATE: float = 0.19 # 19%
EU_TAX_RATE: float = 0.20 # 20%
US_TAX_RATE: float = 0.08 # 8%
# Order processing
MIN_ORDER_AMOUNT: float = 10.0
MAX_ORDER_AMOUNT: float = 10000.0
FREE_SHIPPING_THRESHOLD: float = 50.0
# Inventory
LOW_STOCK_THRESHOLD: int = 10
REORDER_QUANTITY: int = 50
# Subscriptions
TRIAL_PERIOD_DAYS: int = 14
SUBSCRIPTION_GRACE_PERIOD_DAYS: int = 7
constants/ui.py¶
"""
UI-related constants.
Constants for window sizes, colors, animations, etc.
"""
# Window dimensions
WINDOW_WIDTH: int = 1024
WINDOW_HEIGHT: int = 768
MIN_WINDOW_WIDTH: int = 800
MIN_WINDOW_HEIGHT: int = 600
# Widget sizes
ICON_SIZE: int = 24
LARGE_ICON_SIZE: int = 48
BUTTON_HEIGHT: int = 32
TOOLBAR_HEIGHT: int = 40
# Spacing
DEFAULT_MARGIN: int = 10
DEFAULT_SPACING: int = 5
SECTION_SPACING: int = 20
# Animation
ANIMATION_DURATION: int = 250 # milliseconds
FADE_DURATION: int = 150
SLIDE_DURATION: int = 300
# Colors (as hex strings)
PRIMARY_COLOR: str = "#2196F3"
SECONDARY_COLOR: str = "#FFC107"
SUCCESS_COLOR: str = "#4CAF50"
WARNING_COLOR: str = "#FF9800"
ERROR_COLOR: str = "#F44336"
INFO_COLOR: str = "#00BCD4"
# Fonts
DEFAULT_FONT_FAMILY: str = "Segoe UI"
MONOSPACE_FONT_FAMILY: str = "Consolas"
DEFAULT_FONT_SIZE: int = 10
HEADER_FONT_SIZE: int = 14
TITLE_FONT_SIZE: int = 16
constants/files.py¶
"""
File and path-related constants.
"""
from pathlib import Path
# Base directories (relative to project root)
PROJECT_ROOT: Path = Path(__file__).parent.parent.parent
DATA_DIR: Path = PROJECT_ROOT / "data"
PERSISTENT_DATA_DIR: Path = DATA_DIR / "persistent_data"
APP_DATA_DIR: Path = DATA_DIR / ".app_data"
# Subdirectories
BACKUP_DIR: Path = PERSISTENT_DATA_DIR / "backups"
EXPORT_DIR: Path = PERSISTENT_DATA_DIR / "exports"
TEMP_DIR: Path = DATA_DIR / "temp"
LOG_DIR: Path = DATA_DIR / "logs"
# File extensions
PROJECT_FILE_EXTENSION: str = ".myproj"
BACKUP_FILE_EXTENSION: str = ".bak"
TEMP_FILE_EXTENSION: str = ".tmp"
# File name patterns
BACKUP_NAME_PATTERN: str = "backup_{timestamp}.json"
EXPORT_NAME_PATTERN: str = "export_{name}_{timestamp}.csv"
LOG_NAME_PATTERN: str = "{date}.log"
# Allowed file types
ALLOWED_EXTENSIONS: tuple[str, ...] = (
".json",
".xml",
".csv",
".txt",
)
ALLOWED_IMAGE_EXTENSIONS: tuple[str, ...] = (
".png",
".jpg",
".jpeg",
".gif",
".bmp",
)
# File size limits (in bytes)
MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10 MB
MAX_IMAGE_SIZE: int = 5 * 1024 * 1024 # 5 MB
MAX_LOG_SIZE: int = 50 * 1024 * 1024 # 50 MB
# Backup settings
MAX_BACKUP_COUNT: int = 10
BACKUP_RETENTION_DAYS: int = 30
Enumerations (Enums)¶
Why Enums Instead of Plain Strings?¶
# BAD - String constants
USER_STATUS_ACTIVE = "active"
USER_STATUS_INACTIVE = "inactive"
USER_STATUS_SUSPENDED = "suspended"
def check_user(status: str) -> bool:
if status == "activ": # Typo! No error at compile time
return True
return False
# GOOD - Enum
from enum import Enum
class UserStatus(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
SUSPENDED = "suspended"
def check_user(status: UserStatus) -> bool:
if status == UserStatus.ACTIV: # IDE warns immediately!
return True
return False
enums/user.py¶
"""User-related enumerations."""
from enum import Enum, auto
class UserStatus(Enum):
"""User account status."""
ACTIVE = "active"
INACTIVE = "inactive"
SUSPENDED = "suspended"
DELETED = "deleted"
class UserRole(Enum):
"""User role/permission level."""
GUEST = "guest"
USER = "user"
MODERATOR = "moderator"
ADMIN = "admin"
SUPERADMIN = "superadmin"
@property
def level(self) -> int:
"""Get numeric permission level."""
levels = {
self.GUEST: 0,
self.USER: 10,
self.MODERATOR: 50,
self.ADMIN: 90,
self.SUPERADMIN: 100,
}
return levels[self]
def can_access(self, required_level: "UserRole") -> bool:
"""Check if this role has sufficient permissions."""
return self.level >= required_level.level
class SubscriptionTier(Enum):
"""Subscription tier."""
FREE = "free"
BASIC = "basic"
PREMIUM = "premium"
ENTERPRISE = "enterprise"
@property
def max_projects(self) -> int:
"""Get maximum allowed projects for this tier."""
limits = {
self.FREE: 3,
self.BASIC: 10,
self.PREMIUM: 50,
self.ENTERPRISE: -1, # Unlimited
}
return limits[self]
enums/status.py¶
"""Status and state enumerations."""
from enum import Enum, auto
class ProcessingState(Enum):
"""Processing state for async operations."""
IDLE = auto()
RUNNING = auto()
PAUSED = auto()
COMPLETED = auto()
FAILED = auto()
CANCELLED = auto()
@property
def is_active(self) -> bool:
"""Check if state represents active processing."""
return self in (self.RUNNING, self.PAUSED)
@property
def is_terminal(self) -> bool:
"""Check if state is terminal (no further transitions)."""
return self in (self.COMPLETED, self.FAILED, self.CANCELLED)
class FileStatus(Enum):
"""File operation status."""
PENDING = "pending"
UPLOADING = "uploading"
PROCESSING = "processing"
READY = "ready"
ERROR = "error"
class ConnectionState(Enum):
"""Network connection state."""
DISCONNECTED = auto()
CONNECTING = auto()
CONNECTED = auto()
RECONNECTING = auto()
ERROR = auto()
enums/types.py¶
"""Type classifications."""
from enum import Enum
class FileType(Enum):
"""File type classification."""
DOCUMENT = "document"
IMAGE = "image"
VIDEO = "video"
AUDIO = "audio"
ARCHIVE = "archive"
CODE = "code"
DATA = "data"
OTHER = "other"
@classmethod
def from_extension(cls, extension: str) -> "FileType":
"""Determine file type from extension."""
ext = extension.lower().lstrip('.')
if ext in ('jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'):
return cls.IMAGE
elif ext in ('mp4', 'avi', 'mov', 'mkv'):
return cls.VIDEO
elif ext in ('mp3', 'wav', 'flac', 'ogg'):
return cls.AUDIO
elif ext in ('zip', 'rar', '7z', 'tar', 'gz'):
return cls.ARCHIVE
elif ext in ('py', 'js', 'java', 'cpp', 'html', 'css'):
return cls.CODE
elif ext in ('json', 'xml', 'csv', 'sql'):
return cls.DATA
elif ext in ('pdf', 'doc', 'docx', 'txt', 'md'):
return cls.DOCUMENT
else:
return cls.OTHER
class LogLevel(Enum):
"""Logging level."""
DEBUG = 10
INFO = 20
WARNING = 30
ERROR = 40
CRITICAL = 50
def __lt__(self, other: "LogLevel") -> bool:
"""Enable comparison: LogLevel.DEBUG < LogLevel.INFO."""
return self.value < other.value
Configuration Management¶
config/settings.py - Main Configuration Class¶
"""
Application configuration management.
This module provides a centralized configuration system with
environment-specific overrides.
"""
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Dict, Any
import os
import tomli # pip install tomli
@dataclass
class DatabaseConfig:
"""Database configuration."""
host: str = "localhost"
port: int = 5432
name: str = "myapp"
user: str = "user"
password: str = ""
pool_size: int = 5
@dataclass
class APIConfig:
"""External API configuration."""
base_url: str = "https://api.example.com"
api_key: str = ""
timeout: int = 30
max_retries: int = 3
@dataclass
class LoggingConfig:
"""Logging configuration."""
level: str = "INFO"
file: Optional[Path] = None
max_size: int = 10 * 1024 * 1024 # 10 MB
backup_count: int = 5
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
@dataclass
class Settings:
"""
Application settings.
Loads configuration from multiple sources in order of precedence:
1. Environment variables (highest priority)
2. Config file (TOML)
3. Defaults (lowest priority)
Example:
>>> settings = Settings.load()
>>> print(settings.app_name)
'MyApp'
>>> print(settings.database.host)
'localhost'
"""
# Application
app_name: str = "MyApp"
app_version: str = "1.0.0"
environment: str = "development" # development, staging, production
debug: bool = False
# Paths
data_dir: Path = field(default_factory=lambda: Path("data"))
log_dir: Path = field(default_factory=lambda: Path("logs"))
# Sub-configs
database: DatabaseConfig = field(default_factory=DatabaseConfig)
api: APIConfig = field(default_factory=APIConfig)
logging: LoggingConfig = field(default_factory=LoggingConfig)
# Feature flags
enable_auto_save: bool = True
enable_telemetry: bool = False
enable_experimental_features: bool = False
@classmethod
def load(cls, config_file: Optional[Path] = None) -> "Settings":
"""
Load settings from config file and environment.
Args:
config_file: Optional path to TOML config file.
Defaults to 'config/app.toml'.
Returns:
Loaded settings instance.
"""
if config_file is None:
config_file = Path("config/app.toml")
# Load from file
settings_dict = {}
if config_file.exists():
with open(config_file, 'rb') as f:
settings_dict = tomli.load(f)
# Override from environment
settings_dict = cls._apply_env_overrides(settings_dict)
# Create instance
return cls(**settings_dict)
@staticmethod
def _apply_env_overrides(config: Dict[str, Any]) -> Dict[str, Any]:
"""Apply environment variable overrides."""
# Example: MYAPP_DEBUG=true → debug = True
# Example: MYAPP_DATABASE_HOST=prod-db → database.host = "prod-db"
if debug_env := os.getenv("MYAPP_DEBUG"):
config['debug'] = debug_env.lower() == "true"
if db_host := os.getenv("MYAPP_DATABASE_HOST"):
config.setdefault('database', {})['host'] = db_host
if api_key := os.getenv("MYAPP_API_KEY"):
config.setdefault('api', {})['api_key'] = api_key
return config
def save(self, config_file: Path) -> None:
"""Save current settings to TOML file."""
import tomli_w # pip install tomli-w
with open(config_file, 'wb') as f:
tomli_w.dump(self.__dict__, f)
# Global settings instance (singleton pattern)
_settings: Optional[Settings] = None
def get_settings() -> Settings:
"""
Get global settings instance.
Returns:
Settings instance (singleton).
Example:
>>> from myapp.config import get_settings
>>> settings = get_settings()
>>> print(settings.app_name)
"""
global _settings
if _settings is None:
_settings = Settings.load()
return _settings
def reload_settings() -> Settings:
"""
Reload settings from file (useful for testing).
Returns:
Reloaded settings instance.
"""
global _settings
_settings = Settings.load()
return _settings
config/app.toml - Configuration File¶
# Application Configuration
[app]
name = "MyApp"
version = "1.0.0"
environment = "development"
debug = true
[paths]
data_dir = "data"
log_dir = "logs"
[database]
host = "localhost"
port = 5432
name = "myapp_dev"
user = "devuser"
password = ""
pool_size = 5
[api]
base_url = "https://api.example.com"
api_key = "" # Set via environment variable
timeout = 30
max_retries = 3
[logging]
level = "DEBUG"
file = "logs/app.log"
max_size = 10485760 # 10 MB
backup_count = 5
[features]
enable_auto_save = true
enable_telemetry = false
enable_experimental_features = true
Environment-Specific Configs¶
# config/development.py
"""Development environment configuration."""
from myapp.config.settings import Settings
def get_dev_settings() -> Settings:
"""Get development settings."""
settings = Settings()
settings.environment = "development"
settings.debug = True
settings.database.name = "myapp_dev"
settings.logging.level = "DEBUG"
settings.enable_telemetry = False
return settings
# config/production.py
"""Production environment configuration."""
from myapp.config.settings import Settings
def get_prod_settings() -> Settings:
"""Get production settings."""
settings = Settings()
settings.environment = "production"
settings.debug = False
settings.database.host = "prod-db.example.com"
settings.logging.level = "WARNING"
settings.enable_telemetry = True
return settings
User-Facing Strings (i18n)¶
messages/errors.py¶
"""
Error messages.
Centralized error messages for consistent user communication.
"""
# Validation errors
ERROR_EMPTY_FIELD: str = "This field cannot be empty."
ERROR_INVALID_EMAIL: str = "Please enter a valid email address."
ERROR_INVALID_PASSWORD: str = "Password must be at least 8 characters."
ERROR_PASSWORD_MISMATCH: str = "Passwords do not match."
ERROR_USERNAME_TAKEN: str = "This username is already taken."
# File operation errors
ERROR_FILE_NOT_FOUND: str = "The file could not be found."
ERROR_FILE_READ: str = "Failed to read file: {filename}"
ERROR_FILE_WRITE: str = "Failed to write file: {filename}"
ERROR_FILE_PERMISSION: str = "Permission denied: {filename}"
ERROR_FILE_TOO_LARGE: str = "File is too large. Maximum size: {max_size} MB"
# Network errors
ERROR_CONNECTION_FAILED: str = "Failed to connect to server."
ERROR_TIMEOUT: str = "Request timed out. Please try again."
ERROR_NO_INTERNET: str = "No internet connection detected."
# Authentication errors
ERROR_LOGIN_FAILED: str = "Invalid username or password."
ERROR_SESSION_EXPIRED: str = "Your session has expired. Please log in again."
ERROR_UNAUTHORIZED: str = "You don't have permission to perform this action."
# Business logic errors
ERROR_INSUFFICIENT_FUNDS: str = "Insufficient funds for this transaction."
ERROR_INVALID_QUANTITY: str = "Quantity must be greater than zero."
ERROR_ITEM_NOT_AVAILABLE: str = "This item is no longer available."
def format_error(template: str, **kwargs) -> str:
"""
Format error message with parameters.
Args:
template: Error message template with placeholders.
**kwargs: Values to substitute into template.
Returns:
Formatted error message.
Example:
>>> format_error(ERROR_FILE_READ, filename="data.json")
'Failed to read file: data.json'
"""
return template.format(**kwargs)
messages/notifications.py¶
"""
Notification messages.
User-facing notification messages.
"""
# Success notifications
SUCCESS_SAVED: str = "Saved successfully."
SUCCESS_DELETED: str = "{item} has been deleted."
SUCCESS_UPLOADED: str = "File uploaded successfully."
SUCCESS_SENT: str = "Message sent."
# Info notifications
INFO_LOADING: str = "Loading..."
INFO_PROCESSING: str = "Processing your request..."
INFO_SYNCING: str = "Synchronizing data..."
INFO_NO_CHANGES: str = "No changes to save."
# Warning notifications
WARNING_UNSAVED_CHANGES: str = "You have unsaved changes."
WARNING_LOW_STORAGE: str = "Storage space is running low."
WARNING_OFFLINE_MODE: str = "You are currently in offline mode."
# Action confirmations
CONFIRM_DELETE: str = "Are you sure you want to delete {item}?"
CONFIRM_DISCARD: str = "Discard unsaved changes?"
CONFIRM_LOGOUT: str = "Are you sure you want to log out?"
CONFIRM_RESET: str = "This will reset all settings. Continue?"
messages/ui_text.py¶
"""
UI text labels and tooltips.
All user-interface text in one place for easy localization.
"""
# Main menu
MENU_FILE: str = "&File"
MENU_EDIT: str = "&Edit"
MENU_VIEW: str = "&View"
MENU_HELP: str = "&Help"
# File menu items
ACTION_NEW: str = "&New"
ACTION_OPEN: str = "&Open..."
ACTION_SAVE: str = "&Save"
ACTION_SAVE_AS: str = "Save &As..."
ACTION_EXPORT: str = "&Export..."
ACTION_QUIT: str = "&Quit"
# Edit menu items
ACTION_UNDO: str = "&Undo"
ACTION_REDO: str = "&Redo"
ACTION_CUT: str = "Cu&t"
ACTION_COPY: str = "&Copy"
ACTION_PASTE: str = "&Paste"
# Buttons
BUTTON_OK: str = "OK"
BUTTON_CANCEL: str = "Cancel"
BUTTON_APPLY: str = "Apply"
BUTTON_CLOSE: str = "Close"
BUTTON_YES: str = "Yes"
BUTTON_NO: str = "No"
BUTTON_SAVE: str = "Save"
BUTTON_DELETE: str = "Delete"
# Tooltips
TOOLTIP_NEW_PROJECT: str = "Create a new project (Ctrl+N)"
TOOLTIP_OPEN_PROJECT: str = "Open an existing project (Ctrl+O)"
TOOLTIP_SAVE_PROJECT: str = "Save the current project (Ctrl+S)"
TOOLTIP_UNDO: str = "Undo last action (Ctrl+Z)"
TOOLTIP_REDO: str = "Redo last action (Ctrl+Y)"
# Status messages
STATUS_READY: str = "Ready"
STATUS_SAVING: str = "Saving..."
STATUS_LOADING: str = "Loading..."
STATUS_CONNECTED: str = "Connected"
STATUS_DISCONNECTED: str = "Disconnected"
Internationalization (i18n) Setup¶
# messages/i18n.py
"""
Internationalization support.
Provides translation capabilities for all user-facing strings.
"""
import json
from pathlib import Path
from typing import Dict, Optional
# Current language
_current_language: str = "en_US"
_translations: Dict[str, Dict[str, str]] = {}
def load_translation(language: str) -> None:
"""
Load translation file for specified language.
Args:
language: Language code (e.g., 'de_DE', 'en_US').
"""
global _current_language, _translations
translation_file = Path(__file__).parent / "translations" / f"{language}.json"
if translation_file.exists():
with open(translation_file, 'r', encoding='utf-8') as f:
_translations[language] = json.load(f)
_current_language = language
else:
raise FileNotFoundError(f"Translation file not found: {translation_file}")
def t(key: str, **kwargs) -> str:
"""
Translate a string key.
Args:
key: Translation key.
**kwargs: Format parameters.
Returns:
Translated string, or key if translation not found.
Example:
>>> t("error.file_not_found")
'Die Datei wurde nicht gefunden.'
>>> t("error.file_read", filename="data.json")
'Fehler beim Lesen der Datei: data.json'
"""
translation = _translations.get(_current_language, {}).get(key, key)
return translation.format(**kwargs) if kwargs else translation
def get_current_language() -> str:
"""Get current language code."""
return _current_language
# Convenience: Load default language
try:
load_translation("en_US")
except FileNotFoundError:
pass # Fallback to keys
translations/de_DE.json:
{
"error.empty_field": "Dieses Feld darf nicht leer sein.",
"error.invalid_email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"error.file_not_found": "Die Datei wurde nicht gefunden.",
"error.file_read": "Fehler beim Lesen der Datei: {filename}",
"success.saved": "Erfolgreich gespeichert.",
"button.ok": "OK",
"button.cancel": "Abbrechen",
"menu.file": "&Datei",
"menu.edit": "&Bearbeiten"
}
Environment-Specific Values¶
.env - Environment Variables¶
# .env file (never commit to git!)
# Environment
MYAPP_ENVIRONMENT=production
MYAPP_DEBUG=false
# Database
MYAPP_DATABASE_HOST=prod-db.example.com
MYAPP_DATABASE_PORT=5432
MYAPP_DATABASE_NAME=myapp_prod
MYAPP_DATABASE_USER=prod_user
MYAPP_DATABASE_PASSWORD=super_secret_password
# API Keys (sensitive!)
MYAPP_API_KEY=sk_live_abc123def456
MYAPP_SECRET_KEY=secret_xyz789
# External Services
MYAPP_REDIS_URL=redis://localhost:6379
MYAPP_SMTP_HOST=smtp.gmail.com
MYAPP_SMTP_PORT=587
MYAPP_SMTP_USER=no-reply@example.com
MYAPP_SMTP_PASSWORD=email_password
# Feature Flags
MYAPP_ENABLE_TELEMETRY=true
MYAPP_ENABLE_BETA_FEATURES=false
# Logging
MYAPP_LOG_LEVEL=INFO
.env.example - Template (commit this)¶
# .env.example - Copy to .env and fill in values
# Environment
MYAPP_ENVIRONMENT=development
MYAPP_DEBUG=true
# Database
MYAPP_DATABASE_HOST=localhost
MYAPP_DATABASE_PORT=5432
MYAPP_DATABASE_NAME=myapp_dev
MYAPP_DATABASE_USER=devuser
MYAPP_DATABASE_PASSWORD=
# API Keys
MYAPP_API_KEY=
MYAPP_SECRET_KEY=
# External Services
MYAPP_REDIS_URL=redis://localhost:6379
MYAPP_SMTP_HOST=
MYAPP_SMTP_PORT=587
MYAPP_SMTP_USER=
MYAPP_SMTP_PASSWORD=
# Feature Flags
MYAPP_ENABLE_TELEMETRY=false
MYAPP_ENABLE_BETA_FEATURES=false
# Logging
MYAPP_LOG_LEVEL=DEBUG
Loading Environment Variables¶
# utils/env.py
"""
Environment variable utilities.
"""
import os
from pathlib import Path
from typing import Optional, TypeVar, Type
from dotenv import load_dotenv # pip install python-dotenv
T = TypeVar('T')
def load_env_file(env_file: Path = Path(".env")) -> None:
"""
Load environment variables from .env file.
Args:
env_file: Path to .env file.
"""
if env_file.exists():
load_dotenv(env_file)
def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
"""
Get environment variable.
Args:
key: Environment variable name.
default: Default value if not found.
Returns:
Environment variable value or default.
"""
return os.getenv(key, default)
def get_env_bool(key: str, default: bool = False) -> bool:
"""
Get boolean environment variable.
Args:
key: Environment variable name.
default: Default value if not found.
Returns:
Boolean value.
"""
value = get_env(key)
if value is None:
return default
return value.lower() in ('true', '1', 'yes', 'on')
def get_env_int(key: str, default: int = 0) -> int:
"""
Get integer environment variable.
Args:
key: Environment variable name.
default: Default value if not found.
Returns:
Integer value.
"""
value = get_env(key)
if value is None:
return default
try:
return int(value)
except ValueError:
return default
def require_env(key: str) -> str:
"""
Get required environment variable.
Args:
key: Environment variable name.
Returns:
Environment variable value.
Raises:
ValueError: If environment variable is not set.
"""
value = get_env(key)
if value is None:
raise ValueError(f"Required environment variable not set: {key}")
return value
PySide6-Specific Patterns¶
Using Qt Constants¶
# constants/qt_constants.py
"""
PySide6/Qt-specific constants.
"""
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QColor, QFont
# Window flags (for custom dialogs)
DIALOG_FLAGS = (
Qt.WindowType.Dialog |
Qt.WindowType.WindowCloseButtonHint |
Qt.WindowType.WindowTitleHint
)
FRAMELESS_WINDOW_FLAGS = (
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint
)
# Alignment
ALIGN_CENTER = Qt.AlignmentFlag.AlignCenter
ALIGN_LEFT = Qt.AlignmentFlag.AlignLeft
ALIGN_RIGHT = Qt.AlignmentFlag.AlignRight
ALIGN_TOP = Qt.AlignmentFlag.AlignTop
# Icon sizes
ICON_SIZE_SMALL = QSize(16, 16)
ICON_SIZE_MEDIUM = QSize(24, 24)
ICON_SIZE_LARGE = QSize(48, 48)
# Colors (QColor objects)
COLOR_PRIMARY = QColor("#2196F3")
COLOR_SUCCESS = QColor("#4CAF50")
COLOR_WARNING = QColor("#FF9800")
COLOR_ERROR = QColor("#F44336")
# Fonts
FONT_DEFAULT = QFont("Segoe UI", 10)
FONT_HEADER = QFont("Segoe UI", 14, QFont.Weight.Bold)
FONT_MONOSPACE = QFont("Consolas", 10)
# Connection types (for explicit signal connections)
from PySide6.QtCore import Qt
CONNECTION_AUTO = Qt.ConnectionType.AutoConnection
CONNECTION_DIRECT = Qt.ConnectionType.DirectConnection
CONNECTION_QUEUED = Qt.ConnectionType.QueuedConnection
CONNECTION_BLOCKING = Qt.ConnectionType.BlockingQueuedConnection
Stylesheets as Constants¶
# constants/stylesheets.py
"""
Qt stylesheet constants.
Centralized stylesheets for consistent styling.
"""
# Base styles
STYLE_PRIMARY_BUTTON = """
QPushButton {
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 10pt;
}
QPushButton:hover {
background-color: #1976D2;
}
QPushButton:pressed {
background-color: #0D47A1;
}
QPushButton:disabled {
background-color: #BBDEFB;
color: #90CAF9;
}
"""
STYLE_SECONDARY_BUTTON = """
QPushButton {
background-color: white;
color: #2196F3;
border: 2px solid #2196F3;
border-radius: 4px;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #E3F2FD;
}
"""
STYLE_SUCCESS_LABEL = """
QLabel {
color: #4CAF50;
font-weight: bold;
}
"""
STYLE_ERROR_LABEL = """
QLabel {
color: #F44336;
font-weight: bold;
}
"""
# Complete widget styles
STYLE_MAIN_WINDOW = """
QMainWindow {
background-color: #FAFAFA;
}
"""
STYLE_TOOLBAR = """
QToolBar {
background-color: white;
border-bottom: 1px solid #E0E0E0;
spacing: 5px;
}
"""
def get_stylesheet(name: str) -> str:
"""
Get stylesheet by name.
Args:
name: Stylesheet name (without STYLE_ prefix).
Returns:
Stylesheet string.
Example:
>>> button.setStyleSheet(get_stylesheet("PRIMARY_BUTTON"))
"""
return globals().get(f"STYLE_{name.upper()}", "")
Usage in Views¶
# views/main_window.py
from PySide6.QtWidgets import QMainWindow, QPushButton
from myapp.constants.qt_constants import (
DIALOG_FLAGS,
ICON_SIZE_MEDIUM,
COLOR_PRIMARY,
)
from myapp.constants.stylesheets import STYLE_PRIMARY_BUTTON
class MainWindow(QMainWindow):
"""Main application window."""
def __init__(self):
super().__init__()
self._setup_ui()
def _setup_ui(self):
"""Setup user interface."""
self.setWindowFlags(DIALOG_FLAGS)
# Button with constant stylesheet
self.save_button = QPushButton("Save")
self.save_button.setStyleSheet(STYLE_PRIMARY_BUTTON)
self.save_button.setIconSize(ICON_SIZE_MEDIUM)
Best Practices¶
1. Naming Conventions¶
# ✅ GOOD - Clear naming
MAX_RETRY_ATTEMPTS = 3 # Constant: UPPERCASE_WITH_UNDERSCORES
DEFAULT_TIMEOUT_SECONDS = 30
ERROR_MESSAGE_TEMPLATE = "Failed to {action}: {reason}"
class UserStatus(Enum): # Enum class: CapWords
ACTIVE = "active" # Enum members: UPPERCASE
# ❌ BAD - Unclear naming
max_retries = 3 # Looks like a variable
timeout = 30 # No unit
msg = "Error: {0}" # Abbreviated, unclear
2. Group by Context¶
# ✅ GOOD - Grouped by context
# Network timeouts (seconds)
CONNECT_TIMEOUT = 10
READ_TIMEOUT = 30
WRITE_TIMEOUT = 30
# File size limits (bytes)
MAX_FILE_SIZE = 10 * 1024 * 1024
MAX_IMAGE_SIZE = 5 * 1024 * 1024
MAX_VIDEO_SIZE = 100 * 1024 * 1024
# ❌ BAD - Mixed topics
MAX_SIZE = 10000000
TIMEOUT = 30
MAX_IMG = 5000000
CONNECT = 10
3. Document Units¶
# ✅ GOOD - Units in name or comment
REQUEST_TIMEOUT_SECONDS = 30
FILE_SIZE_LIMIT_MB = 10
CACHE_TTL_MINUTES = 60
# Or with comments
MAX_RETRIES = 3 # Number of retry attempts
DEFAULT_TIMEOUT = 30.0 # Timeout in seconds
BUFFER_SIZE = 8192 # Buffer size in bytes
# ❌ BAD - Ambiguous units
TIMEOUT = 30 # Seconds? Milliseconds?
MAX_SIZE = 10 # Bytes? KB? MB?
4. Avoid Duplication¶
# ✅ GOOD - Calculated from base constants
BYTES_PER_KB = 1024
BYTES_PER_MB = BYTES_PER_KB * 1024
BYTES_PER_GB = BYTES_PER_MB * 1024
MAX_FILE_SIZE_MB = 10
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * BYTES_PER_MB
# ❌ BAD - Duplicated values
MAX_FILE_SIZE_MB = 10
MAX_FILE_SIZE_BYTES = 10485760 # Magic number! Duplicate!
5. Type Safety with Enums¶
# ✅ GOOD - Type-safe with Enum
from enum import Enum
class ConnectionType(Enum):
WIFI = "wifi"
ETHERNET = "ethernet"
CELLULAR = "cellular"
def connect(connection_type: ConnectionType) -> bool:
if connection_type == ConnectionType.WIFI: # IDE autocomplete!
return setup_wifi()
# ...
# ❌ BAD - String constants (typo-prone)
CONN_WIFI = "wifi"
CONN_ETHERNET = "ethernet"
def connect(connection_type: str) -> bool:
if connection_type == "wiffi": # Typo! No error!
return setup_wifi()
6. Default Values in Functions¶
# ✅ GOOD - Reference constant
from myapp.constants import DEFAULT_TIMEOUT
def fetch_data(url: str, timeout: int = DEFAULT_TIMEOUT) -> bytes:
"""Fetch data with configurable timeout."""
# If DEFAULT_TIMEOUT changes, all functions update automatically
pass
# ❌ BAD - Duplicate magic number
def fetch_data(url: str, timeout: int = 30) -> bytes:
pass
def fetch_json(url: str, timeout: int = 30) -> dict:
pass
# Now you need to change 30 in multiple places!
7. Configurable vs. Fixed Constants¶
# constants/app.py - Fixed constants
APP_NAME = "MyApp" # Never changes
MIN_PASSWORD_LENGTH = 8 # Business rule
# config/settings.py - Configurable values
@dataclass
class Settings:
max_retries: int = 3 # User can configure
timeout: int = 30 # Environment-specific
debug: bool = False # Deployment-specific
Refactoring Strategy¶
Step 1: Identify¶
# Script to find magic numbers
import ast
import sys
class MagicNumberFinder(ast.NodeVisitor):
"""Find potential magic numbers in code."""
def visit_Num(self, node):
# Ignore obvious constants
if node.n not in (0, 1, -1, 2, 10, 100, 1000):
print(f"Line {node.lineno}: {node.n}")
self.generic_visit(node)
# Usage: python find_magic_numbers.py myfile.py
Step 2: Incremental Refactoring¶
# BEFORE
def validate_password(password: str) -> bool:
if len(password) < 8: # Magic number!
return False
return True
def create_account(username: str, password: str) -> bool:
if len(password) < 8: # Duplicate magic number!
raise ValueError("Password too short")
# ...
# STEP 1: Extract constant
MIN_PASSWORD_LENGTH = 8 # Local constant
def validate_password(password: str) -> bool:
if len(password) < MIN_PASSWORD_LENGTH:
return False
return True
# STEP 2: Move to constants module
from myapp.constants import MIN_PASSWORD_LENGTH
def validate_password(password: str) -> bool:
if len(password) < MIN_PASSWORD_LENGTH:
return False
return True
def create_account(username: str, password: str) -> bool:
if len(password) < MIN_PASSWORD_LENGTH:
raise ValueError("Password too short")
# ...
Step 3: Testing¶
# tests/test_constants.py
"""Test that constants are used correctly."""
import pytest
from myapp.constants import MIN_PASSWORD_LENGTH
def test_min_password_length_enforced():
"""Verify password length is enforced."""
from myapp.auth import validate_password
# Test with exact minimum length
assert validate_password("a" * MIN_PASSWORD_LENGTH)
# Test with one less than minimum
assert not validate_password("a" * (MIN_PASSWORD_LENGTH - 1))
Complete Example: Order Processing¶
# constants/business.py
"""Business logic constants for order processing."""
# Pricing
BASE_PRICE_PER_UNIT = 9.99
BULK_ORDER_THRESHOLD = 100
BULK_DISCOUNT_RATE = 0.10
PREMIUM_DISCOUNT_RATE = 0.15
# Shipping
FREE_SHIPPING_THRESHOLD = 50.0
STANDARD_SHIPPING_COST = 5.99
EXPRESS_SHIPPING_COST = 12.99
# Limits
MIN_ORDER_QUANTITY = 1
MAX_ORDER_QUANTITY = 1000
MIN_ORDER_AMOUNT = 10.0
# Tax rates by region
TAX_RATE_DEFAULT = 0.19
TAX_RATE_EU = 0.20
TAX_RATE_US = 0.08
# enums/order.py
"""Order-related enumerations."""
from enum import Enum
class OrderStatus(Enum):
"""Order processing status."""
PENDING = "pending"
CONFIRMED = "confirmed"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class ShippingMethod(Enum):
"""Shipping method."""
STANDARD = "standard"
EXPRESS = "express"
PICKUP = "pickup"
@property
def cost(self) -> float:
"""Get shipping cost."""
from myapp.constants.business import (
STANDARD_SHIPPING_COST,
EXPRESS_SHIPPING_COST,
)
costs = {
self.STANDARD: STANDARD_SHIPPING_COST,
self.EXPRESS: EXPRESS_SHIPPING_COST,
self.PICKUP: 0.0,
}
return costs[self]
# models/order.py
"""Order processing logic."""
from typing import List
from myapp.constants.business import (
BASE_PRICE_PER_UNIT,
BULK_ORDER_THRESHOLD,
BULK_DISCOUNT_RATE,
PREMIUM_DISCOUNT_RATE,
FREE_SHIPPING_THRESHOLD,
MIN_ORDER_QUANTITY,
MAX_ORDER_QUANTITY,
TAX_RATE_DEFAULT,
)
from myapp.enums.order import OrderStatus, ShippingMethod
class Order:
"""Order with professional constant usage."""
def __init__(self, quantity: int, is_premium: bool = False):
if quantity < MIN_ORDER_QUANTITY:
raise ValueError(f"Minimum quantity is {MIN_ORDER_QUANTITY}")
if quantity > MAX_ORDER_QUANTITY:
raise ValueError(f"Maximum quantity is {MAX_ORDER_QUANTITY}")
self.quantity = quantity
self.is_premium = is_premium
self.status = OrderStatus.PENDING
self.shipping_method = ShippingMethod.STANDARD
def calculate_subtotal(self) -> float:
"""Calculate subtotal before discounts."""
return self.quantity * BASE_PRICE_PER_UNIT
def calculate_discount(self) -> float:
"""Calculate applicable discount."""
subtotal = self.calculate_subtotal()
# Premium discount
if self.is_premium:
return subtotal * PREMIUM_DISCOUNT_RATE
# Bulk discount
if self.quantity >= BULK_ORDER_THRESHOLD:
return subtotal * BULK_DISCOUNT_RATE
return 0.0
def calculate_shipping(self) -> float:
"""Calculate shipping cost."""
subtotal = self.calculate_subtotal()
# Free shipping for large orders
if subtotal >= FREE_SHIPPING_THRESHOLD:
return 0.0
return self.shipping_method.cost
def calculate_total(self) -> float:
"""Calculate final total with all adjustments."""
subtotal = self.calculate_subtotal()
discount = self.calculate_discount()
shipping = self.calculate_shipping()
tax = (subtotal - discount) * TAX_RATE_DEFAULT
return subtotal - discount + shipping + tax
Tool Support¶
VS Code Extension: Better Comments¶
# ! Critical constant - do not change without review
MAX_RETRIES = 3
# ? Consider making this configurable
DEFAULT_TIMEOUT = 30
# TODO: Move to config file
API_URL = "https://api.example.com"
# * Important: Used in multiple modules
MIN_PASSWORD_LENGTH = 8
Pre-commit Hook: Check for Magic Numbers¶
# scripts/check_magic_numbers.py
"""Check for potential magic numbers in staged files."""
import ast
import sys
ALLOWED_NUMBERS = {0, 1, -1, 2, 10, 100, 1000}
def check_file(filepath):
with open(filepath) as f:
tree = ast.parse(f.read())
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.Num):
if node.n not in ALLOWED_NUMBERS:
issues.append((node.lineno, node.n))
return issues
if __name__ == "__main__":
# Check staged Python files
pass
Summary¶
Decision Matrix¶
| Scenario | Solution |
|---|---|
| Business rule (discount rate) | constants/business.py |
| Status value (pending, active) | enums/status.py with Enum |
| UI dimensions (window size) | constants/ui.py |
| Environment-specific (API URL) | config/settings.py + .env |
| Secret (API key) | .env file ONLY |
| User message | messages/errors.py |
| File path | constants/files.py |
| Feature flag | config/settings.py |
Key Takeaways¶
- ✅ Always use named constants instead of magic numbers/strings
- ✅ Organize by context (business, ui, files, etc.)
- ✅ Use Enums for type safety when values form a set
- ✅ Separate configuration from constants
- ✅ Never commit secrets - use environment variables
- ✅ Document units in names or comments
- ✅ Calculate derived values from base constants
- ✅ Centralize user-facing strings for i18n
- ✅ Test constant usage in critical code
- ✅ Refactor gradually - don't do everything at once
Professional code uses NO magic numbers. Every value has a name and a reason.