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 leading underscore marks deliberate module-private state (_cache = {}).
  3. A dunder such as __version__ takes the same exemption.
  4. A single-character name reads as a matrix or a scalar by mathematical convention.
  5. A lambda binds a callable.
  6. A binding inside an if TYPE_CHECKING: block is declared for the type checker alone.

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 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.

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.

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.

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

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

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.