A flake8 plugin that helps simplify Python code by detecting patterns that can be improved for readability and maintainability
npx @tessl/cli install tessl/pypi-flake8-simplify@0.22.0A flake8 plugin that helps simplify Python code by detecting patterns that can be improved for readability and maintainability. It provides over 40 different rules that analyze Python AST and suggest code simplifications, including combining isinstance calls, using context managers, replacing nested if statements, utilizing built-in functions, and modernizing Python constructs.
pip install flake8-simplifyastor>=0.1, flake8>=3.7, importlib-metadata>=0.9 (Python < 3.8)The plugin automatically registers with flake8 through entry points (registered as "SIM") and doesn't require explicit imports in typical usage scenarios. Users simply install the package and run flake8.
For direct programmatic access to the plugin class (advanced usage only):
from flake8_simplify import PluginHowever, the standard workflow requires no imports - the plugin integrates automatically via the entry point defined in pyproject.toml:
[project.entry-points]
"flake8.extension" = {SIM = "flake8_simplify:Plugin"}flake8-simplify integrates seamlessly with flake8. After installation, it automatically analyzes your code when running flake8:
# Install the plugin
pip install flake8-simplify
# Run flake8 on your code
flake8 your_file.py
# Or run on entire directory
flake8 .Example output:
./foo.py:10:12: SIM101 Multiple isinstance-calls which can be merged into a single call for variable 'value'
./bar.py:25:8: SIM108 Use ternary operator instead of if-else-assignment
./baz.py:42:1: SIM115 Use context handler for opening filesThe plugin follows flake8's standard plugin architecture:
Each rule function analyzes specific AST node types and returns violations as tuples containing line number, column offset, and error message. This design allows the plugin to efficiently check multiple simplification patterns in a single AST traversal.
The main plugin interface for flake8 integration, providing the entry point for code analysis and rule execution.
class Plugin:
"""Main flake8 plugin class."""
name = __name__ # Class attribute: Plugin identifier (__name__ resolves to "flake8_simplify")
version = importlib_metadata.version(__name__) # Class attribute: Plugin version from package metadata
def __init__(self, tree: ast.AST):
"""
Initialize plugin with AST tree to analyze.
Parameters:
- tree: ast.AST - Python AST tree to analyze
"""
def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
"""
Execute analysis and yield violations.
Returns:
Generator yielding tuples of (line, column, message, plugin_type)
"""
class Visitor(ast.NodeVisitor):
"""AST visitor implementing all simplification rules."""
errors: List[Tuple[int, int, str]] # Collected violations
def __init__(self):
"""Initialize visitor with empty error list."""
# Visit methods for each AST node type
def visit_Assign(self, node: ast.Assign) -> Any: ...
def visit_Call(self, node: ast.Call) -> Any: ...
def visit_With(self, node: ast.With) -> Any: ...
def visit_Expr(self, node: ast.Expr) -> None: ...
def visit_BoolOp(self, node: ast.BoolOp) -> None: ...
def visit_If(self, node: ast.If) -> None: ...
def visit_For(self, node: ast.For) -> None: ...
def visit_Subscript(self, node: ast.Subscript) -> None: ...
def visit_Try(self, node: ast.Try) -> None: ...
def visit_UnaryOp(self, node_v: ast.UnaryOp) -> None: ...
def visit_IfExp(self, node: ast.IfExp) -> None: ...
def visit_Compare(self, node: ast.Compare) -> None: ...
def visit_ClassDef(self, node: ast.ClassDef) -> None: ...
def add_meta(root: ast.AST, level: int = 0) -> None:
"""
Add parent and sibling metadata to AST nodes.
Adds parent, previous_sibling, and next_sibling attributes to all AST nodes
to enable rule analysis that requires knowledge of node relationships.
Parameters:
- root: ast.AST - Root AST node to process
- level: int - Recursion depth (default: 0)
"""Rules for detecting and simplifying variable assignment patterns.
def get_sim904(node: ast.Assign) -> List[Tuple[int, int, str]]:
"""
SIM904: Dict assignment that can be simplified.
Detects: a = {}; a['key'] = 'value'
Suggests: a = {'key': 'value'}
Parameters:
- node: ast.Assign - Assignment node to analyze
Returns:
List of (line, column, message) tuples
"""
def get_sim909(node: Assign) -> List[Tuple[int, int, str]]:
"""
SIM909: Remove reflexive assignments.
Detects: foo = foo
Suggests: Remove redundant assignment
Parameters:
- node: Assign - Extended assignment node with metadata
Returns:
List of (line, column, message) tuples
"""Rules for simplifying boolean expressions and logical operations.
def get_sim101(node: ast.BoolOp) -> List[Tuple[int, int, str]]:
"""
SIM101: Merge multiple isinstance calls.
Detects: isinstance(a, int) or isinstance(a, float)
Suggests: isinstance(a, (int, float))
Parameters:
- node: ast.BoolOp - Boolean operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim109(node: ast.BoolOp) -> List[Tuple[int, int, str]]:
"""
SIM109: Use tuple for multiple equality checks.
Detects: value == a or value == b or value == c
Suggests: value in (a, b, c)
Parameters:
- node: ast.BoolOp - Boolean operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim220(node: ast.BoolOp) -> List[Tuple[int, int, str]]:
"""
SIM220: Replace contradictory expressions with False.
Detects: a and not a
Suggests: False
Parameters:
- node: ast.BoolOp - Boolean operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim221(node: ast.BoolOp) -> List[Tuple[int, int, str]]:
"""
SIM221: Replace tautological expressions with True.
Detects: a or not a
Suggests: True
Parameters:
- node: ast.BoolOp - Boolean operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim222(node: ast.BoolOp) -> List[Tuple[int, int, str]]:
"""
SIM222: Simplify OR with True.
Detects: ... or True
Suggests: True
Parameters:
- node: ast.BoolOp - Boolean operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim223(node: ast.BoolOp) -> List[Tuple[int, int, str]]:
"""
SIM223: Simplify AND with False.
Detects: ... and False
Suggests: False
Parameters:
- node: ast.BoolOp - Boolean operation node
Returns:
List of (line, column, message) tuples
"""Rules for optimizing function calls and method invocations.
def get_sim115(node: Call) -> List[Tuple[int, int, str]]:
"""
SIM115: Use context manager for file operations.
Detects: f = open('file.txt'); ...
Suggests: with open('file.txt') as f: ...
Parameters:
- node: Call - Extended call node with metadata
Returns:
List of (line, column, message) tuples
"""
def get_sim901(node: ast.Call) -> List[Tuple[int, int, str]]:
"""
SIM901: Remove unnecessary bool() wrapper.
Detects: bool(a == b)
Suggests: a == b
Parameters:
- node: ast.Call - Function call node
Returns:
List of (line, column, message) tuples
"""
def get_sim905(node: ast.Call) -> List[Tuple[int, int, str]]:
"""
SIM905: Use list literal instead of string split.
Detects: "a,b,c".split(",")
Suggests: ["a", "b", "c"]
Parameters:
- node: ast.Call - Function call node
Returns:
List of (line, column, message) tuples
"""
def get_sim906(node: ast.Call) -> List[Tuple[int, int, str]]:
"""
SIM906: Merge nested os.path.join calls.
Detects: os.path.join(os.path.join(a, b), c)
Suggests: os.path.join(a, b, c)
Parameters:
- node: ast.Call - Function call node
Returns:
List of (line, column, message) tuples
"""
def get_sim910(node: Call) -> List[Tuple[int, int, str]]:
"""
SIM910: Remove explicit None default from dict.get.
Detects: dict.get(key, None)
Suggests: dict.get(key)
Parameters:
- node: Call - Extended call node with metadata
Returns:
List of (line, column, message) tuples
"""
def get_sim911(node: ast.Call) -> List[Tuple[int, int, str]]:
"""
SIM911: Use dict.items() instead of zip(keys(), values()).
Detects: zip(dict.keys(), dict.values())
Suggests: dict.items()
Parameters:
- node: ast.Call - Function call node
Returns:
List of (line, column, message) tuples
"""Rules for simplifying if statements, loops, and conditional expressions.
def get_sim102(node: ast.If) -> List[Tuple[int, int, str]]:
"""
SIM102: Combine nested if statements.
Detects: if a: if b: ...
Suggests: if a and b: ...
Parameters:
- node: ast.If - If statement node
Returns:
List of (line, column, message) tuples
"""
def get_sim103(node: ast.If) -> List[Tuple[int, int, str]]:
"""
SIM103: Return boolean condition directly.
Detects: if condition: return True else: return False
Suggests: return condition
Parameters:
- node: ast.If - If statement node
Returns:
List of (line, column, message) tuples
"""
def get_sim108(node: If) -> List[Tuple[int, int, str]]:
"""
SIM108: Use ternary operator for assignments.
Detects: if condition: x = a else: x = b
Suggests: x = a if condition else b
Parameters:
- node: If - Extended if node with metadata
Returns:
List of (line, column, message) tuples
"""
def get_sim114(node: ast.If) -> List[Tuple[int, int, str]]:
"""
SIM114: Combine if-elif with same body using OR.
Detects: if a: body elif b: body
Suggests: if a or b: body
Parameters:
- node: ast.If - If statement node
Returns:
List of (line, column, message) tuples
"""
def get_sim116(node: ast.If) -> List[Tuple[int, int, str]]:
"""
SIM116: Use dictionary instead of if-elif equality checks.
Detects: Long if-elif chains with equality comparisons
Suggests: Dictionary lookup pattern
Parameters:
- node: ast.If - If statement node
Returns:
List of (line, column, message) tuples
"""
def get_sim401(node: ast.If) -> List[Tuple[int, int, str]]:
"""
SIM401: Use dict.get() with default instead of if-else.
Detects: if key in dict: x = dict[key] else: x = default
Suggests: x = dict.get(key, default)
Parameters:
- node: ast.If - If statement node
Returns:
List of (line, column, message) tuples
"""
def get_sim908(node: ast.If) -> List[Tuple[int, int, str]]:
"""
SIM908: Use dict.get() for key existence checks.
Detects: if key in dict: dict[key]
Suggests: dict.get(key)
Parameters:
- node: ast.If - If statement node
Returns:
List of (line, column, message) tuples
"""Rules for optimizing for loops and replacing them with more Pythonic constructs.
def get_sim104(node: ast.For) -> List[Tuple[int, int, str]]:
"""
SIM104: Use yield from for generator delegation.
Detects: for item in iterable: yield item
Suggests: yield from iterable
Parameters:
- node: ast.For - For loop node
Returns:
List of (line, column, message) tuples
"""
def get_sim110_sim111(node: ast.For) -> List[Tuple[int, int, str]]:
"""
SIM110/SIM111: Use any() or all() for loop patterns.
SIM110 - Detects: for x in iterable: if condition: return True
SIM110 - Suggests: return any(condition for x in iterable)
SIM111 - Detects: for x in iterable: if condition: return False
SIM111 - Suggests: return all(not condition for x in iterable)
Parameters:
- node: ast.For - For loop node
Returns:
List of (line, column, message) tuples
"""
def get_sim113(node: For) -> List[Tuple[int, int, str]]:
"""
SIM113: Use enumerate instead of manual counter.
Detects: i = 0; for item in iterable: ...; i += 1
Suggests: for i, item in enumerate(iterable): ...
Parameters:
- node: For - Extended for node with metadata
Returns:
List of (line, column, message) tuples
"""Rules for simplifying comparison operations and unary expressions.
def get_sim118(node: ast.Compare) -> List[Tuple[int, int, str]]:
"""
SIM118: Use 'key in dict' instead of 'key in dict.keys()'.
Detects: key in dict.keys()
Suggests: key in dict
Parameters:
- node: ast.Compare - Comparison node
Returns:
List of (line, column, message) tuples
"""
def get_sim300(node: ast.Compare) -> List[Tuple[int, int, str]]:
"""
SIM300: Use natural comparison order (avoid Yoda conditions).
Detects: 42 == age
Suggests: age == 42
Parameters:
- node: ast.Compare - Comparison node
Returns:
List of (line, column, message) tuples
"""
def get_sim201(node: UnaryOp) -> List[Tuple[int, int, str]]:
"""
SIM201: Use '!=' instead of 'not =='.
Detects: not (a == b)
Suggests: a != b
Parameters:
- node: UnaryOp - Extended unary operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim202(node: UnaryOp) -> List[Tuple[int, int, str]]:
"""
SIM202: Use '==' instead of 'not !='.
Detects: not (a != b)
Suggests: a == b
Parameters:
- node: UnaryOp - Extended unary operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim203(node: UnaryOp) -> List[Tuple[int, int, str]]:
"""
SIM203: Use 'not in' instead of 'not (in)'.
Detects: not (a in b)
Suggests: a not in b
Parameters:
- node: UnaryOp - Extended unary operation node
Returns:
List of (line, column, message) tuples
"""
def get_sim208(node: UnaryOp) -> List[Tuple[int, int, str]]:
"""
SIM208: Remove double negation.
Detects: not (not a)
Suggests: a
Parameters:
- node: UnaryOp - Extended unary operation node
Returns:
List of (line, column, message) tuples
"""Rules for simplifying conditional expressions (ternary operators).
def get_sim210(node: ast.IfExp) -> List[Tuple[int, int, str]]:
"""
SIM210: Use bool() instead of True if condition else False.
Detects: True if condition else False
Suggests: bool(condition)
Parameters:
- node: ast.IfExp - Ternary expression node
Returns:
List of (line, column, message) tuples
"""
def get_sim211(node: ast.IfExp) -> List[Tuple[int, int, str]]:
"""
SIM211: Use 'not condition' instead of False if condition else True.
Detects: False if condition else True
Suggests: not condition
Parameters:
- node: ast.IfExp - Ternary expression node
Returns:
List of (line, column, message) tuples
"""
def get_sim212(node: ast.IfExp) -> List[Tuple[int, int, str]]:
"""
SIM212: Use natural order for ternary expressions.
Detects: b if not a else a
Suggests: a if a else b
Parameters:
- node: ast.IfExp - Ternary expression node
Returns:
List of (line, column, message) tuples
"""Rules for improving exception handling and resource management patterns.
def get_sim105(node: ast.Try) -> List[Tuple[int, int, str]]:
"""
SIM105: Use contextlib.suppress for try-except-pass.
Detects: try: risky_operation() except SomeException: pass
Suggests: from contextlib import suppress; with suppress(SomeException): risky_operation()
Parameters:
- node: ast.Try - Try statement node
Returns:
List of (line, column, message) tuples
"""
def get_sim107(node: ast.Try) -> List[Tuple[int, int, str]]:
"""
SIM107: Avoid return in try/except and finally.
Detects: return statements in both try/except and finally blocks
Suggests: Restructure control flow to avoid confusing returns
Parameters:
- node: ast.Try - Try statement node
Returns:
List of (line, column, message) tuples
"""
def get_sim117(node: ast.With) -> List[Tuple[int, int, str]]:
"""
SIM117: Merge nested with statements.
Detects: with a: with b: ...
Suggests: with a, b: ...
Parameters:
- node: ast.With - With statement node
Returns:
List of (line, column, message) tuples
"""Rules for modernizing class definitions and type annotations.
def get_sim120(node: ast.ClassDef) -> List[Tuple[int, int, str]]:
"""
SIM120: Use modern class definition syntax.
Detects: class FooBar(object):
Suggests: class FooBar:
Parameters:
- node: ast.ClassDef - Class definition node
Returns:
List of (line, column, message) tuples
"""
def get_sim907(node: ast.Subscript) -> List[Tuple[int, int, str]]:
"""
SIM907: Use Optional[Type] instead of Union[Type, None].
Detects: Union[str, None]
Suggests: Optional[str]
Parameters:
- node: ast.Subscript - Subscript node (for type annotations)
Returns:
List of (line, column, message) tuples
"""Rules for improving environment variable usage and expression patterns.
def get_sim112(node: ast.Expr) -> List[Tuple[int, int, str]]:
"""
SIM112: Use CAPITAL environment variable names.
Detects: os.environ['path']
Suggests: os.environ['PATH']
Parameters:
- node: ast.Expr - Expression node
Returns:
List of (line, column, message) tuples
"""Enhanced AST node classes that include parent and sibling relationships for rule analysis.
class UnaryOp(ast.UnaryOp):
"""Extended UnaryOp with parent tracking."""
parent: ast.Expr # Parent node reference
def __init__(self, orig: ast.UnaryOp):
"""Copy constructor from original UnaryOp node."""
class Call(ast.Call):
"""Extended Call with parent tracking."""
parent: ast.Expr # Parent node reference
def __init__(self, orig: ast.Call):
"""Copy constructor from original Call node."""
class If(ast.If):
"""Extended If with parent tracking."""
parent: ast.Expr # Parent node reference
def __init__(self, orig: ast.If):
"""Copy constructor from original If node."""
class For(ast.For):
"""Extended For with parent/sibling tracking."""
parent: ast.AST # Parent node reference
previous_sibling: ast.AST # Previous sibling node
def __init__(self, orig: ast.For):
"""Copy constructor from original For node."""
class Assign(ast.Assign):
"""Extended Assign with parent/sibling tracking."""
parent: ast.AST # Parent node reference
previous_sibling: ast.AST # Previous sibling node
orig: ast.Assign # Original node reference
def __init__(self, orig: ast.Assign):
"""Copy constructor from original Assign node."""Helper functions for analyzing and transforming AST nodes.
def to_source(node: Union[None, ast.expr, ast.Expr, ast.withitem, ast.slice, ast.Assign]) -> str:
"""
Convert AST node to source code string.
Parameters:
- node: AST node or None
Returns:
Source code string representation
"""
def strip_parenthesis(string: str) -> str:
"""
Remove surrounding parentheses from string.
Parameters:
- string: String to process
Returns:
String without outer parentheses
"""
def strip_triple_quotes(string: str) -> str:
"""
Convert triple quotes to double quotes.
Parameters:
- string: String with potential triple quotes
Returns:
String with normalized quotes
"""
def use_double_quotes(string: str) -> str:
"""
Convert single quotes to double quotes.
Parameters:
- string: String with potential single quotes
Returns:
String with double quotes
"""
def is_body_same(body1: List[ast.stmt], body2: List[ast.stmt]) -> bool:
"""
Check if two statement lists are equivalent.
Parameters:
- body1: First list of AST statements
- body2: Second list of AST statements
Returns:
True if equivalent, False otherwise
"""
def is_stmt_equal(a: ast.stmt, b: ast.stmt) -> bool:
"""
Deep comparison of AST statements.
Parameters:
- a: First AST statement
- b: Second AST statement
Returns:
True if equal, False otherwise
"""
def get_if_body_pairs(node: ast.If) -> List[Tuple[ast.expr, List[ast.stmt]]]:
"""
Extract condition-body pairs from if-elif chain.
Parameters:
- node: If statement node
Returns:
List of (condition, body) tuples
"""
def is_constant_increase(expr: ast.AugAssign) -> bool:
"""
Check if augmented assignment increments by constant 1.
Parameters:
- expr: Augmented assignment node
Returns:
True if increments by 1, False otherwise
"""
def is_exception_check(node: ast.If) -> bool:
"""
Check if if-statement is an exception check pattern.
Parameters:
- node: If statement node
Returns:
True if raises exception, False otherwise
"""
def is_same_expression(a: ast.expr, b: ast.expr) -> bool:
"""
Check if two expressions are identical.
Parameters:
- a: First expression node
- b: Second expression node
Returns:
True if same, False otherwise
"""
def expression_uses_variable(expr: ast.expr, var: str) -> bool:
"""
Check if expression references a variable.
Parameters:
- expr: Expression node
- var: Variable name to check
Returns:
True if variable is used, False otherwise
"""
def body_contains_continue(stmts: List[ast.stmt]) -> bool:
"""
Check if statement list contains continue statements.
Parameters:
- stmts: List of statements
Returns:
True if contains continue, False otherwise
"""
def _get_duplicated_isinstance_call_by_node(node: ast.BoolOp) -> List[str]:
"""
Get a list of isinstance arguments which could be shortened (used by SIM101).
Analyzes boolean operations to find isinstance calls that can be combined.
For example: isinstance(a, int) or isinstance(a, float) can become isinstance(a, (int, float))
Parameters:
- node: ast.BoolOp - Boolean operation node containing isinstance calls
Returns:
List of variable names that have multiple isinstance calls
"""Python version compatibility constants for different AST node types. These constants handle differences between Python versions in AST node representation.
# ast.Constant in Python 3.8+, ast.NameConstant in Python 3.6-3.7
if hasattr(ast, 'NameConstant'):
BOOL_CONST_TYPES = (ast.Constant, ast.NameConstant)
AST_CONST_TYPES = (ast.Constant, ast.NameConstant, ast.Str, ast.Num)
STR_TYPES = (ast.Constant, ast.Str)
else:
BOOL_CONST_TYPES = (ast.Constant,)
AST_CONST_TYPES = (ast.Constant,)
STR_TYPES = (ast.Constant,)from typing import Any, Generator, List, Tuple, Type, Union, DefaultDict
from collections import defaultdict
import ast
import itertools
import astor
import importlib_metadata # For Python < 3.8 compatibility
# Type aliases for rule function return values
RuleViolation = Tuple[int, int, str] # (line, column, message)
RuleResult = List[RuleViolation]
# Extended AST node types with metadata
ExtendedASTNode = Union[UnaryOp, Call, If, For, Assign]