Skip to content

miscased-constants ​

miscased-constants reports a module-level assignment that binds a fixed value under a name PEP 8 would write in SCREAMING_CASE, and suggests that form as a display-only rename, so a public global written as max_retries stops reading as mutable state where nothing writes it again. It is the other half of the pair reassigned-constants opens, one rule per side of the casing contract, and it reports without rewriting, because renaming a module constant breaks every importer outside the file.

A name draws the report when it is longer than one character, has no leading underscore, is not already SCREAMING_CASE, and binds a value the module never reassigns. Several kinds of binding stay quiet:

  1. A global produced by a call is effectful rather than fixed (logger = get_logger(__name__), app = build()).
  2. A list, dict, or set display the module writes into holds mutable state, whether the write is an item assignment, an item deletion, or a mutating method such as append or update, applied to the name or to an item the display holds (dispatch_table = {} above a def storing dispatch_table[ob_type] = pickle_function). A display the module never writes into still draws the rename, as each codec module's encoding_map does.
  3. A leading underscore marks deliberate module-private state (_cache = {}).
  4. A dunder such as __version__ takes the same exemption.
  5. A single-character name reads as a matrix or a scalar by mathematical convention.
  6. A lambda binds a callable.
  7. A binding inside an if TYPE_CHECKING: block is declared for the type checker alone.
  8. A name the module's own __all__ lists is the spelling importers use, so a rename would break them (project under __all__ = ["project"]). Every name stays quiet where the module's __all__ cannot be listed from the source, as with a write holding anything other than string literals, a target unpacking into __all__, or a write inside a compound statement such as an if, a try, or a def. An attribute read off __all__ anywhere in the module (__all__.append(name)), a write or a deletion through a subscript of it, and a statement other than an assignment binding the name, an import among them, each quiet every name the same way.

A notebook is skipped whole, because a cell's top-level assignments are working variables.

A type alias is never renamed, because SCREAMING_CASE is the one casing an alias must not take, and a target annotated TypeAlias is exempt outright. Otherwise the rule tells an alias from a constant by the value rather than the name, so a value that points at something already built is an alias (Pen = Turtle, open = TarFile.open, Interval = Union[int, float]), and PEP 604's Interval = int | float reads the same way on both sides of the |. A value choosing through or or and evaluates to one of its operands, so it is an alias wherever every operand is one (scanstring = c_scanstring or py_scanstring), whereas encoding = preferred or "utf-8" builds its fallback and stays a constant. A value that builds something new is a constant and still draws the rename, covering a literal, an f-string, a collection display, and an arithmetic expression.

A subscript can be a constant or an alias, as SETTINGS["db"] and Literal["read"] show, so three checks settle it. A slice, a dunder, or an unwrapped integer, bool, or bytes marks the base as data rather than a type, leaving Literal[1, 2, 3] and every Annotated argument past the first alone. A base assigned a {...} literal in the same file marks a lookup, whereas an imported NDArray[float] stays a type. A name the module truth-tests, order-compares, or does arithmetic on binds data, whereas a name used in an annotation is a type whatever else reads it. None of the three checks can turn a constant into an alias, so an unresolved value keeps its name and draws no report, and database = SETTINGS["db"] goes unreported wherever SETTINGS comes from another module.

Configuration ​

KeyTypeDefaultMeaning
enabledbooltrueTurns the rule on or off.
allow-patternstring""Constant names exempted from the lint, a glob matched against the whole name, such as old-style bare aliases.

The allow-pattern glob is empty by default and exempts nothing beyond the structural carve-outs above. A project that keeps a name out of SCREAMING_CASE on purpose sets the pattern to spare that name, which is a different job from never renaming a type alias, and only the second happens without configuration.

The Canonical Case ​

max_retries = 5 binds an inert literal that nothing in the module reassigns, so the name is a constant whose casing never says so. The lint reports it and suggests MAX_RETRIES without rewriting anything, because renaming a module constant breaks importers outside the file.

max_retries = 5
python

More Examples ​

maxRetries = 5 is a module constant in camelCase. The lint reports it and suggests MAX_RETRIES, splitting the name at its case boundary where a plain uppercasing would produce MAXRETRIES. The same word-boundary split covers a PascalCase MaxRetries.

SETTINGS["db"] and Literal["read"] are the same subscript form, so the rule resolves each base against the rest of the file to tell the two apart. SETTINGS is assigned a dict literal one line above, which makes database a lookup into data, and the lint reports it with the DATABASE rename. Nothing in the file binds Literal, so Mode reads as a type alias and passes.

pickle stores into dispatch_table, remove_extension deletes from extension_registry, and add_filter calls insert on filters, so each display holds state the module changes after binding it. None of the three is reported, because a SCREAMING_CASE name would label that state as fixed. Nothing writes into encoding_map, so it is still reported with the suggested ENCODING_MAP rename.

default_encoding is bound behind the sys.platform == "win32" guard. The lint reports it with the suggested DEFAULT_ENCODING rename, because a top-level if is still module scope and the scan descends into it. Only a def, a class, or an if TYPE_CHECKING: block ends the descent.

Palette, Timeout, Banner, and Ceiling bind a dict display, a literal, an f-string, and an arithmetic expression, and every one of those constructs new data. All four are reported with SCREAMING_CASE renames, because classification follows the value rather than how the target is spelled. This is the converse of the carve-out that exempts a bare class alias, and it is what keeps dispatch tables and derived strings inside the lint's reach.

The | in a module-level assignment can spell a PEP 604 union or a bitwise or, so the rule reads both sides. Interval = int | float joins two type names and passes as a type alias, whereas permissions = 0o644 | 0o111 combines two octal literals into plain data and is reported with the PERMISSIONS rename.

scanstring takes c_scanstring where it is truthy and py_scanstring otherwise, and decoder takes c_decoder where it is falsy and py_decoder otherwise, so each value is one of the objects its operands already name rather than a value the assignment builds. Neither name is reported, because the rule reads a choice through or or and as an alias wherever every operand is one. encoding falls back to the string literal "utf-8", which builds a value, so it is still reported with the suggested ENCODING rename.

timeout: int = 30 binds a module constant just as an unannotated assignment would. The annotation changes nothing, and the lint reports the name with the suggested TIMEOUT rename. Only a bare annotation with no value binds nothing and goes unreported.

The slice [1:], the dunder [__name__], and the bare integers [0] and [-1] never appear inside a type's parameters, so each of those subscripts reads as an index into data. alt_path_sep, oldmod, first_sep, and last_sep are all reported with SCREAMING_CASE renames. The check reads only the code in front of it, with no import map and no type inference, and dict[str, int], Literal["read", "write"], and the signed Literal[-1] still pass as the types they are.

path_sep = os.sep and opener = tarfile.TarFile.open are the same attribute-read form, and nothing in either value says which one binds data, so how the module uses each name settles it. The module truth-tests path_sep in if path_sep:, a pointless operation on a class, so path_sep is reported with the PATH_SEP rename, whereas opener is only ever called and passes. Vec and Crate read as types from their values alone, one parametrizing the imported NDArray and the other the locally declared Box.

project, retries, and timeout are listed in the module's __all__ through an assignment and an augmented assignment, so each one is the spelling importers use. None of the three is reported, because a SCREAMING_CASE rename would break every importer, and a from … import * of the module would raise AttributeError on the __all__ entry left naming nothing. verbose sits outside the list and is still reported with the suggested VERBOSE rename.

Defaults = {"retries": 3} and timeout = 30 sit at module scope under allow-pattern = "[A-Z][a-z]*". Defaults matches the pattern and passes, whereas timeout matches nothing and is reported with the suggested TIMEOUT rename. The pattern defaults to empty and exempts no name at all, so a project sets it for the names it keeps out of SCREAMING_CASE on purpose.

A subscript holding an integer, a bool, or a bytes string cannot be a type expression, so first_level = LEVELS[1], default_flag = OPTIONS[True], and raw_codec = CODECS[b"gzip"] all read as data lookups and each is reported with a SCREAMING_CASE rename.

Literal is the one construct whose subscript does contain such values, which is what keeps Literal[1, 2, 3], Literal[True], Literal[-1], and Literal[b"gzip"] read as types. PEP 593 allows every Annotated argument after the first to be arbitrary metadata, so the Field(gt=0) call does not disqualify Positive, and the [int, str] parameter list of Handler, the ... of AnyHandler, and the empty tuple[()] of Empty all pass as written too.

No Change

json_type = dict is bound inside an if TYPE_CHECKING: block, a region that never executes at runtime and exists only for the type checker. The lint never reports json_type, because a binding there is type-only scaffolding rather than runtime configuration.

No Change

Vector: TypeAlias = list[float] carries a TypeAlias annotation, which marks the target as a type rather than a constant. The annotation alone exempts Vector, before the rule reads the assigned value, whose list[float] type expression would exempt it anyway.

No Change

max_retries: int declares a type without binding a value. The line is not reported, because no constant exists for the lint to classify, and the rule reads only assignments that bind one.

No Change

logger and app each bind the result of a call, so evaluating either one runs code rather than reading a fixed value. The lint reports neither name despite the lowercase, because a constant is an inert value and a call-produced global is not one.

No Change

Colors, Kind, Node, and int_ each alias a class, and the module uses them in four ways that look like data handling. None of the four is reported, because each use is an operation a class supports too. An Enum class is itself iterable, covering for color in Colors:, while type(value) == Kind is an equality check, base is Node an identity check, and int_.from_bytes a method read, each as valid on a type as on data. Reading any of the four as a data read would rename a real type.

No Change

max_retries = 5 sits inside configure, where the lowercase name is not reported, because the scan does not descend into a def and a function local is a working variable rather than a module constant.

No Change

counter is assigned twice at module level, making it mutable state rather than a constant. The lint passes over it, because its lowercase name is exactly right. The mirror case belongs to reassigned-constants, which reports a SCREAMING_CASE name that a later write contradicts.

No Change

Tree: TypeAlias = "list[Tree]" forward-references itself as a string, because the name is unbound while its own value evaluates. A string literal constructs data, so reading the value alone would report the name. The TypeAlias annotation is the one signal that marks Tree a type, and it exempts the name.

No Change

MAX_RETRIES = 5 assigns a module-level constant whose name is already SCREAMING_CASE. The lint reports nothing, this being the form every reported case suggests renaming to.

No Change

The bare name Turtle, the subscript list[float], and the attribute TarFile.open each refer to an object that already exists, which is the form a PEP 484 type alias takes. None of the three assignments is reported, because the rule reads the value rather than the target, which is what exempts the lowercase opener alongside the PascalCase Pen.

No Change

add_entry appends to the list at menudefs[0][1] and register assigns a slice of the list at handlers[event], so each write lands on an object the display holds rather than on the display itself. Neither name is reported, because the rule follows the subscripts back to the name the write starts from, and the display's contents change either way.

No Change

The rule reads only the single-name assignment form, one plain name bound to one value. The chained first = second = 1, the tuple unpacking first, second = 1, 2, and the attribute target config.timeout = 30 all pass, whatever the casing of their names, because each falls outside that form.

No Change

__all__ holds a starred name besides a string, so no static read of the file lists what the module exports. Neither project nor retries is reported, because either could be one of the names names supplies, and renaming an exported name breaks every importer.

No Change

x = 3 and n = 100 assign single-letter names at module scope, the form that usually names a mathematical scalar. The lint exempts single-character names, because uppercasing one leaves a lone capital, and by mathematical convention a lone capital reads as a matrix rather than a constant, so the rename would mislead.

No Change

_cache = {} opens with a leading underscore, the marker for deliberate module-private state, and __version__ = "1.0" is a dunder whose casing is fixed by convention. Neither name is reported, because both are excluded before the casing check runs.

For per-line opt-outs, the Suppression chapter covers the # prose: ignore[miscased-constants] directive.