CQ013: NoGlobalState

Overview

Property Value
ID CQ013
Name NoGlobalState
Group code-quality
Severity WARNING

Description

Flags module-level mutable state that can lead to hard-to-debug issues, thread-safety problems, and unpredictable behavior.

Module-level mutable variables are problematic because:

  • Shared state across imports: All modules importing your code share the same mutable objects
  • Thread-safety issues: Concurrent access to mutable globals can cause race conditions
  • Testing difficulties: Tests may affect each other through shared state
  • Hidden dependencies: Functions that modify global state have implicit side effects
  • Unpredictable behavior: Order of operations matters when global state is involved

What it checks

The check scans all Python files in the package (excluding test files, hidden directories, and __pycache__), identifies module-level assignments to mutable types:

  • List literals: [] or [1, 2, 3]
  • Dict literals: {} or {"key": "value"}
  • Set literals: {1, 2, 3}
  • Mutable constructor calls: list(), dict(), set(), defaultdict(), OrderedDict(), Counter(), deque()

Result states

  • PASSED: No mutable global state found
  • FAILED: Mutable global variables detected
  • NOT_APPLICABLE: Package directory not found

Ignored patterns

The following are NOT flagged:

  • UPPER_CASE constants: Assumed to be treated as immutable by convention
  • __all__ list: Standard Python module export list
  • Type annotations without values: items: list[str] (no assignment)
  • ClassVar annotations: Class-level variables that are explicitly typed as ClassVar
# These ARE flagged as mutable global state
cache = {}
pending_items = []
active_connections = set()
counters = defaultdict(int)

# These are NOT flagged
ALLOWED_EXTENSIONS = ["py", "txt"]  # UPPER_CASE constant
__all__ = ["func1", "func2"]  # Standard export list
items: list[str]  # Type annotation only

How to fix

Use functions or classes to encapsulate state

# Before - mutable global state
cache = {}

def get_data(key):
    if key not in cache:
        cache[key] = fetch_from_database(key)
    return cache[key]

# After - encapsulate in a class
class DataCache:
    def __init__(self):
        self._cache = {}

    def get(self, key):
        if key not in self._cache:
            self._cache[key] = fetch_from_database(key)
        return self._cache[key]

# Create instance where needed
cache = DataCache()

Use function-local state or closures

# Before - global mutable state
results = []

def process_item(item):
    result = transform(item)
    results.append(result)
    return result

# After - return values instead of mutating globals
def process_item(item):
    return transform(item)

def process_all(items):
    return [process_item(item) for item in items]

Use dependency injection

# Before - hidden dependency on global state
_config = {}

def initialize(settings):
    global _config
    _config = settings

def get_setting(key):
    return _config[key]

# After - explicit dependency injection
def create_app(config):
    def get_setting(key):
        return config[key]
    return get_setting

Use UPPER_CASE for intentional constants

# If you intentionally want a module-level collection that won't change,
# use UPPER_CASE to signal it's a constant (check will ignore it)
SUPPORTED_FORMATS = ["json", "yaml", "toml"]
DEFAULT_SETTINGS = {"timeout": 30, "retries": 3}

Use functools.lru_cache for memoization

from functools import lru_cache

# Before - manual cache as global dict
_cache = {}

def expensive_computation(arg):
    if arg not in _cache:
        _cache[arg] = do_expensive_work(arg)
    return _cache[arg]

# After - use lru_cache decorator
@lru_cache(maxsize=128)
def expensive_computation(arg):
    return do_expensive_work(arg)

Why WARNING severity?

This check is a WARNING because:

  • Some legitimate use cases exist for module-level state (singletons, registries)
  • Legacy code may rely on this pattern
  • The check cannot determine if the state is truly problematic

However, you should prefer explicit state management for:

  • Better testability
  • Thread safety
  • Code clarity

Configuration

Skip this check

[tool.pycmdcheck]
skip = ["CQ013"]

CLI

pycmdcheck --skip CQ013

References