Each finding names a stable rule id you can suppress, disable, or tune. Priorities run from 1 (highest) to 5 (lowest). Property values below are catalogue defaults; finding messages state the comparison that fired.
Start with the built-in python policy. It keeps useful checks and raises LongVariable.maximum from 20 to 35 so descriptive names are not punished. Add opinionated when you want the stricter set python leaves out.
messpy only reads syntax. It does not import your packages, execute your code, or consult your type checker. Leading-underscore private names, dunder names, conventional receivers, short index/coordinate/exception names, constants, and type-parameter declarations are handled with ordinary Python expectations. Names that cannot be known statically are not guessed.
| Component | Rule | Priority | Default properties | What it catches |
|---|---|---|---|---|
naming |
ShortClassName |
3 | minimum=3 |
Flags class names shorter than minimum so cryptic one- and two-letter types stand out. |
naming |
LongClassName |
3 | maximum=40 |
Flags class names longer than maximum so sprawling type names get shortened or split. |
naming |
ShortVariable |
3 | minimum=3 |
Flags parameter, property, and variable names shorter than minimum, with ordinary short-index exemptions. |
naming |
LongVariable |
3 | maximum=20 |
Flags parameter, property, and variable names longer than maximum. |
naming |
ShortMethodName |
3 | minimum=3 |
Flags function and method names shorter than minimum. |
naming |
ConstantNamingConventions |
3 | — | Flags module/class constants that are not UPPER_CASE when messpy can identify them statically. |
naming |
BooleanGetMethodName |
3 | — | Flags proven-boolean methods that still use a get_ prefix instead of a Python boolean prefix. Needs an explicit bool annotation or a conservative literal-boolean body. Accepts prefixes such as is_, has_, can_, should_, was_, will_, and did_. |
naming |
ConstructorWithNameAsEnclosingClass |
3 | — | Does nothing on Python—there is no separately named constructor declaration to check. The id remains loadable so shared policies do not break; it never fires on Python. |
codesize |
CyclomaticComplexity |
3 | reportlevel=10 |
Flags callables whose decision count plus one is at least reportlevel (branches, loops, handlers, comprehensions, pattern matches, and similar). |
codesize |
NPathComplexity |
3 | minimum=200 |
Flags callables whose syntax-only independent path count is at least minimum. |
codesize |
ExcessiveParameterList |
3 | minimum=10 |
Flags callables with at least minimum parameters; positional-only, keyword-only, and variadic forms each count once. |
codesize |
ExcessiveMethodLength |
3 | minimum=100 |
Flags callables with at least minimum physical lines, including signature and body. |
codesize |
ExcessiveClassLength |
3 | minimum=1000, ignore-whitespace=false |
Flags classes with at least minimum lines; set ignore-whitespace=true to count nonblank lines only. |
codesize |
ExcessivePublicCount |
3 | minimum=45 |
Flags classes whose public fields plus concrete methods reach at least minimum. |
codesize |
TooManyFields |
3 | maxfields=15 |
Flags classes with more than maxfields statically declared or assigned fields. |
codesize |
TooManyMethods |
3 | maxmethods=25, ignorepattern=(^(set|get|is|has|with))i |
Flags classes with more than maxmethods concrete methods after ignorepattern exclusions (default skips common getter/setter-style names). |
codesize |
TooManyPublicMethods |
3 | maxmethods=10, ignorepattern=(^(set|get|is|has|with))i |
Flags classes with more than maxmethods public concrete methods after ignorepattern. |
codesize |
ExcessiveClassComplexity |
3 | maximum=50 |
Flags classes whose summed concrete-method cyclomatic complexity is at least maximum. |
unusedcode |
UnusedLocalVariable |
3 | — | Flags locals and comprehension bindings with no proven use inside their lexical scope. |
unusedcode |
UnusedFormalParameter |
3 | — | Flags callable parameters with no proven use; conventional underscore-unused names stay quiet. |
unusedcode |
UnusedPrivateField |
3 | — | Flags underscore-private fields with no proven class use, backing off when dynamic access or framework patterns make certainty impossible. |
unusedcode |
UnusedPrivateMethod |
3 | — | Flags underscore-private methods with no proven class use, backing off when dynamic access, framework patterns, or an unresolved base class makes certainty impossible. |
cleancode |
BooleanArgumentFlag |
1 | exceptions=, ignorepattern= |
Flags boolean parameters that often force forked call-site behavior; allowlist names with exceptions or ignorepattern. Uses annotations and defaults only—no runtime type lookup. |
cleancode |
ElseExpression |
1 | — | Flags an else that follows a branch which always returns, raises, continues, or breaks—usually dead or misleading structure. |
cleancode |
StaticAccess |
1 | exceptions=, ignorepattern= |
Flags class-like static calls that are clearer as ordinary functions or instance methods; allowlist with exceptions or ignorepattern. |
cleancode |
IfStatementAssignment |
1 | — | Flags assignment expressions (:=) used directly in if or while conditions. Only the condition of if / while, not every := in the file. |
cleancode |
DuplicatedArrayKey |
2 | — | Flags repeated statically equal, hashable keys in a dictionary literal (the shared rule name still says Array). Dictionary literals only; dynamic or unhashable keys are not guessed. |
design |
ExitExpression |
1 | — | Flags process-exit calls such as sys.exit / os._exit once per lexical scope, including common aliases. Follows visible sys / os / builtin exit aliases and respects local shadowing. |
design |
GotoStatement |
1 | — | Does nothing on Python—there is no goto statement. The id remains loadable so shared policies do not break; it never fires on Python. |
design |
CountInLoopExpression |
2 | — | Flags builtin len(...) calls inside while conditions. Ordinary for loops over range(len(...)) stay quiet. |
design |
DevelopmentCodeFragment |
2 | unwanted-functions=, markers=TODO,FIXME,HACK |
Flags leftover debug calls and comment markers. breakpoint, builtins.breakpoint, and pdb.set_trace are always on, including their from-imported and aliased forms; add more via unwanted-functions. Default markers: TODO,FIXME,HACK (case-insensitive). Resolves import aliases, including unwanted-functions, before matching. An unresolved spelling is not a match. User-defined or rebound breakpoint names stay quiet. |
design |
EmptyCatchBlock |
2 | — | Flags except handlers whose body is only pass or .... |
design |
CouplingBetweenObjects |
2 | maximum=13 |
Flags classes that touch at least maximum distinct external types/modules via imports, bases, decorators, annotations, or references. Counts syntax references only—never imports the referenced modules. |
design |
GlobalVariable |
1 | report-immutable=false |
Flags module bindings that are actually mutated. Set report-immutable=true to also report initialized module state, including Final. Mutation-based by default so imports and true constants stay quiet. |
design |
LackOfCohesionOfMethods |
3 | maximum=1 |
Flags classes whose methods form more than maximum disconnected groups (LCOM4) via shared instance state and receiver calls. Properties, trivial accessors, static/class methods, and abstract/protocol stubs do not inflate the score alone. |
controversial |
CamelCaseClassName |
1 | — | Flags non-private class names that are not Python CapWords. Rule id is historical; the check is CapWords for classes. |
controversial |
CamelCaseMethodName |
1 | allow-underscore=false, allow-underscore-test=false |
Flags non-private function and method names that are not Python snake_case. Rule id is historical; the check is snake_case for functions and methods. Underscore-compatibility properties stay loadable and do not disable that requirement. |
controversial |
CamelCasePropertyName |
1 | allow-underscore=false, allow-underscore-test=false |
Flags non-private property names that are not Python snake_case. Rule id is historical; the check is snake_case for properties. Underscore-compatibility properties stay loadable and do not disable that requirement. |
controversial |
CamelCaseParameterName |
1 | allow-underscore=false |
Flags non-private parameter names that are not Python snake_case, after ordinary receiver and short-name exemptions. Rule id is historical; the check is snake_case for parameters. self / cls, short indexes/coordinates/exceptions, and *args / **kwargs-style names stay quiet. |
controversial |
CamelCaseVariableName |
1 | allow-underscore=false |
Flags non-private, non-constant variable names that are not Python snake_case. Rule id is historical; the check is snake_case for variables. Keyword trailing underscores and conventional short names stay quiet. |
explicitness |
ImplicitInput |
3 | — | Flags callable and module bodies that read mutable outer or captured storage, or ambient sources such as input, open for reading, os.environ, sys.argv, standard streams, the clock, and random / secrets calls. Reads through parameters are explicit input. Definitions, imports, builtins, and proven immutable constants stay quiet. Mutable uppercase or Final containers remain inputs. A name that is both imported and assigned, such as an ImportError fallback, counts as an import. Known reads, writes, and unknown operations propagate through resolved same-file calls to a finite fixed point. Function, lambda, coroutine, and generator bodies belong to their own owner. Cold body effects transfer at established forcing operations rather than creation. Load-time effects belong to <module>. Only literal dead branches are pruned. Fixed scalar captures and fixed fresh built-in container captures stay quiet inside the supported subset. |
explicitness |
ImplicitOutput |
3 | — | Flags callable and module bodies that send data out other than by return: global / nonlocal writes, changes to outer or argument objects, and ambient sinks such as print, open for writing, stream writes, logging, and subprocess or file-system calls. Mutation is recognized by assignment, del, and common mutator methods such as append and update. Ownership is observed at the mutation site. Aliases retain their origins, and branches join possible origins. Fresh shallow copies have private outer storage and can retain caller-owned elements. Writes to those reachable elements remain outputs. Private outer mutation stays quiet. The method receiver is left to ImplicitInstanceOutput; excluded receiver facts retain UnknownEffect. |
explicitness |
UnknownEffect |
3 | — | Flags unresolved calls, injected callbacks, opaque external calls, and receiver effects excluded by the selected instance-state policy. Known actions can also have unknown effects. Uses the native AST, lexical resolver, and call graph. Same-file calculations and modeled standard operations stay quiet. Opaque descriptor, protocol, virtual, decorator, and deferred execution retain uncertainty. A method name alone does not prove a custom method pure. See the classifier contract for the analyzed subset. |
strictexplicitness |
ImplicitInstanceInput |
3 | — | Flags methods that read instance or class state through self / cls. Calls to other methods on the receiver stay quiet. |
strictexplicitness |
ImplicitInstanceOutput |
3 | — | Flags methods that assign, delete, or mutate instance or class state through self / cls. __init__ and __post_init__ stay quiet. __new__ does not, because its receiver is the class. |
onion |
DomainAction |
2 | domain= |
Flags an action in a domain-layer module: an implicit input, an implicit output, a write to self / cls outside a constructor, ambient I/O at import time, or a call to another action in the same module. Set domain to a comma-separated list of shell patterns matched against the file's absolute path. * matches across directories, so the result does not depend on the working directory or the scan root. The usual form is */package/domain/*. The check reuses ImplicitInput, ImplicitOutput, and ImplicitInstanceOutput, so those rules do not have to be loaded. Reads of self / cls stay quiet. A spread finding is anchored at the call and names the chain, for example calls save_report(), which writes the implicit output print. A suppression on a direct action does not stop that action from spreading to callers in the same file. Calls into other files are not followed. A callback or repository passed as an argument is judged only by the name-based mutator list: repo.add(order) is an action, while notify(order) and repo.save(order) stay quiet. Annotations that Python evaluates at import time are included: module and class variable annotations, and signature annotations on module-level functions and methods. from __future__ import annotations and Python 3.14 defer those expressions, so they stay quiet. A type alias value is not evaluated at import time. Annotations on a nested function are not import-time actions. |
onion |
DomainOuterImport |
2 | domain=, outer-layers= |
Flags an import, in a domain-layer module, of a module named in outer-layers. A name matches that module and its submodules; a trailing .* is shorthand for the module and its submodules. Set domain the same way as DomainAction, and outer-layers to a comma-separated list of module names such as myapp.infra,requests. import, from ... import, and relative imports are checked, including imports inside functions and under if TYPE_CHECKING:. A type-only import still couples the domain layer to that outer layer. A relative import is resolved by walking parent directories that contain __init__.py; when it cannot be resolved, the imported names are matched as written. An empty domain, or an empty outer-layers on this rule, is a ruleset error. |
Pass one or more of these as the third CLI argument. Comma-separate to compose.
naming— Whether names are long enough, short enough, and conventionally shaped.ShortClassName,LongClassName,ShortVariable,LongVariable,ShortMethodName,ConstantNamingConventions,BooleanGetMethodName,ConstructorWithNameAsEnclosingClassunusedcode— Locals, parameters, and private members that appear never used.UnusedPrivateField,UnusedLocalVariable,UnusedPrivateMethod,UnusedFormalParametercleancode— Small structural smells that make code harder to read and change.BooleanArgumentFlag,ElseExpression,StaticAccess,IfStatementAssignment,DuplicatedArrayKeydesign— Module and class design hazards: exits, empties, coupling, globals, cohesion.ExitExpression,GotoStatement,CountInLoopExpression,DevelopmentCodeFragment,EmptyCatchBlock,CouplingBetweenObjects,GlobalVariable,LackOfCohesionOfMethodspython— Recommended low-noise default for ordinary projects.CyclomaticComplexity,NPathComplexity,ExcessiveMethodLength,ExcessiveClassLength,ExcessiveParameterList,ExcessivePublicCount,TooManyFields,TooManyMethods,TooManyPublicMethods,ExcessiveClassComplexity,ShortClassName,LongClassName,LongVariable(maximum=35),ShortMethodName,ConstantNamingConventions,BooleanGetMethodName,UnusedPrivateField,UnusedLocalVariable,UnusedPrivateMethod,IfStatementAssignment,DuplicatedArrayKey,DevelopmentCodeFragment,EmptyCatchBlock,CouplingBetweenObjects,GlobalVariable,LackOfCohesionOfMethods,CamelCaseClassName,CamelCaseMethodName,CamelCasePropertyName,CamelCaseParameterName,CamelCaseVariableNamecontroversial— Strict CapWords classes and snake_case identifiers.CamelCaseClassName,CamelCaseMethodName,CamelCasePropertyName,CamelCaseParameterName,CamelCaseVariableNameopinionated— Stricter checks left out ofpython; combine aspython,opinionated.ShortVariable,UnusedFormalParameter,BooleanArgumentFlag,ElseExpression,StaticAccess,CountInLoopExpression,ExitExpressioncodesize— How big and branchy callables and classes have become.CyclomaticComplexity,NPathComplexity,ExcessiveMethodLength,ExcessiveClassLength,ExcessiveParameterList,ExcessivePublicCount,TooManyFields,TooManyMethods,TooManyPublicMethods,ExcessiveClassComplexityexplicitness— Known implicit inputs and outputs, propagated callable effects, and unresolved operations reported asUnknownEffect.ImplicitInput,ImplicitOutput,UnknownEffectstrictexplicitness—explicitnessplus instance and class state, so methods countself/clsdata as implicit too.ImplicitInput,ImplicitOutput,UnknownEffect,ImplicitInstanceInput,ImplicitInstanceOutputonion— Actions and outer-layer imports in the domain layer. Configuredomainandouter-layersbefore use.DomainAction,DomainOuterImport
onion is not part of python. A team names its domain layer and its outer layers once:
<ruleset name="team">
<rule ref="onion">
<properties>
<property name="domain" value="*/myapp/domain/*" />
<property name="outer-layers" value="myapp.infra,myapp.web,requests,sqlalchemy" />
</properties>
</rule>
</ruleset>domain matches the file's absolute path. outer-layers matches a module and its submodules, so myapp.infra covers myapp.infra.db. A trailing .* is accepted as shorthand for the same root and its submodules.
A clean report is not proof that the domain layer is pure. The check has three blind spots:
- It stays inside one file, so a domain function that calls an action defined in another module is not reported by
DomainAction. - An injected callback or repository is not treated as an action. Only a mutator-named call on an argument, such as
repo.add(order), is reported. - Mutators are recognized by name (
append,add, and the same list asImplicitOutput).