A plugin for flake8 finding likely bugs and design problems in your program.
npx @tessl/cli install tessl/pypi-flake8-bugbear@24.12.0A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle. This plugin implements over 70 opinionated linting rules that catch subtle bugs, design problems, and code quality issues that standard tools miss.
pip install flake8-bugbearThe plugin automatically integrates with flake8 once installed. For programmatic usage:
from bugbear import BugBearCheckerOnce installed, flake8-bugbear automatically runs as part of flake8:
# Install the plugin
pip install flake8-bugbear
# Run flake8 - bugbear rules are automatically included
flake8 mycode.py
# Check that the plugin is loaded
flake8 --versionimport ast
from bugbear import BugBearChecker
# Parse some Python code
code = '''
def example():
try:
risky_operation()
except: # B001: bare except
pass
'''
tree = ast.parse(code)
lines = code.splitlines()
# Create and run the checker
checker = BugBearChecker(tree=tree, filename="example.py", lines=lines)
errors = list(checker.run())
for error in errors:
print(f"{error.lineno}:{error.col} {error.message}")flake8-bugbear follows flake8's plugin architecture:
The plugin registers with flake8 via entry point: flake8.extension = {B = "bugbear:BugBearChecker"}
flake8-bugbear supports the following configuration options:
[flake8]
# Extend the list of immutable function calls for B008/B039
extend-immutable-calls = frozenset,tuple
# Customize classmethod decorators for B902
classmethod-decorators = classmethod,my_classmethod_decoratorThe BugBearChecker class provides the primary interface for flake8 integration and programmatic usage.
class BugBearChecker:
"""
Main checker class implementing flake8 plugin interface.
Attributes:
name (str): Plugin name "flake8-bugbear"
version (str): Plugin version
tree (ast.AST): AST tree to analyze
filename (str): Name of file being analyzed
lines (List[str]): Source code lines
max_line_length (int): Maximum line length setting
options (object): Configuration options
"""
def run(self):
"""
Main entry point that yields error instances.
Yields:
Error objects with line number, column, and message
"""
def load_file(self) -> None:
"""Load file content into tree and lines attributes."""
def gen_line_based_checks(self):
"""
Generator for line-based lint checks.
Yields:
Error objects for line-based violations
"""
@classmethod
def adapt_error(cls, e):
"""
Adapt extended error namedtuple to be compatible with flake8.
Args:
e: Error namedtuple instance
Returns:
Adapted error tuple
"""
@staticmethod
def add_options(optmanager) -> None:
"""
Register configuration options with flake8.
Args:
optmanager: flake8 option manager instance
"""
def should_warn(self, code: str) -> bool:
"""
Check if warning code should be reported.
Args:
code: Error code (e.g., "B001")
Returns:
True if warning should be reported
"""flake8-bugbear implements over 70 specific error detection rules. Each rule has a unique code (B001-B950) and targets specific code patterns that are likely bugs or poor design choices.
# B001: Bare except clauses
try:
risky_operation()
except: # Error: Use "except Exception:" instead
pass
# B002: Unary prefix increment
++n # Error: Python doesn't support this, use n += 1
# B003: os.environ assignment
os.environ = {} # Error: Use os.environ.clear() instead
# B004: hasattr() with __call__
hasattr(obj, '__call__') # Error: Use callable(obj) instead
# B005: strip() with multi-character strings
text.strip('abc') # Error: Misleading, use replace() or regex
# B006: Mutable default arguments
def func(items=[]): # Error: Use items=None, then items = items or []
pass
# B007: Unused loop variables
for i in range(10): # Error: If unused, name it _i
print("hello")
# B008: Function calls in argument defaults
def func(timestamp=time.time()): # Error: Called once at definition
pass
# B009-B010: getattr/setattr with known attributes
getattr(obj, 'attr') # Error: Use obj.attr directly
setattr(obj, 'attr', val) # Error: Use obj.attr = val
# B011: assert False
assert False # Error: Use raise AssertionError() instead
# B012: Control flow in finally blocks
try:
operation()
finally:
return value # Error: Will silence exceptions
# B013-B014: Exception handling issues
except (ValueError,): # B013: Redundant tuple
except (ValueError, ValueError): # B014: Duplicate exception types# B015: Redundant comparisons
if x == True: # Error: Use if x: instead
# B016: Raising in finally
try:
operation()
finally:
raise Exception() # Error: Will override original exception
# B017: pytest.raises without match
with pytest.raises(ValueError): # Error: Add match= parameter
risky_operation()
# B018: Useless expressions
x + 1 # Error: Statement has no effect
# B019: lru_cache without parameters
@lru_cache # Error: Use @lru_cache() with parentheses
# B020: Loop variable usage outside loop
for i in range(10):
items.append(i)
print(i) # Error: i is not guaranteed to be defined
# B021: f-string in format()
"{}".format(f"{x}") # Error: Use f-string directly
# B022: Useless contextlib.suppress
with contextlib.suppress(): # Error: No exception types specified
operation()
# B023: Function definition scope issues
for i in range(3):
def func():
return i # Error: Will always return final loop value
# B024: Abstract methods without @abstractmethod
class Base:
def method(self):
raise NotImplementedError() # Error: Use @abstractmethod
# B025: Duplicate except handlers
try:
operation()
except ValueError:
handle_error()
except ValueError: # Error: Duplicate handler
handle_error()
# B026: Star-arg unpacking after keyword argument
def func(a, b, **kwargs, *args): # Error: *args after **kwargs is discouraged
pass
# B027: Empty abstract method without decorator
class Base:
def method(self): # Error: Empty method should use @abstractmethod
pass
# B028: warnings.warn without stacklevel
import warnings
warnings.warn("message") # Error: Should specify stacklevel=2
# B029: Empty except tuple
try:
operation()
except (): # Error: Empty tuple catches nothing
pass# B030: Except handler names
except ValueError as e: # OK
except ValueError as ValueError: # Error: Shadowing exception class
# B031: itertools.islice with generators
list(itertools.islice(generator, 10)) # Error: Consider using itertools.takewhile
# B032: Possible unintentional type annotations
def func():
x: int # Error: Missing assignment, use x: int = 0
# B033: Duplicate set elements
{1, 2, 1} # Error: Duplicate element
# B034: re.sub without flags
re.sub(r'pattern', 'replacement', text) # Error: Consider adding flags
# B035: Static key in dict comprehension
{key: value for item in items} # Error: key doesn't depend on item
# B036: Found f-string with incorrect prefix
rf"regex {pattern}" # Error: Use fr"regex {pattern}" instead
# B037: __exit__ return value
def __exit__(self, exc_type, exc_val, exc_tb):
return True # Error: Usually should return None
# B039: contextlib.suppress with BaseException
with contextlib.suppress(BaseException): # Error: Too broad
operation()
# B040: Exception caught without being used
try:
operation()
except ValueError as e: # Error: e is never used
log("Error occurred")
# B041: Repeated key-value pairs in dict literals
{"key": 1, "key": 2} # Error: Duplicate key# B901: return in generator
def generator():
yield 1
return 2 # Error: Use return without value
# B902: Invalid first argument names
class Example:
def method(bad_self): # Error: Should be 'self'
pass
@classmethod
def classmethod(bad_cls): # Error: Should be 'cls'
pass
# B903: Data class without slots
@dataclass
class Example: # Error: Consider @dataclass(slots=True) for performance
value: int
# B904: re-raise without from
try:
operation()
except ValueError:
raise RuntimeError("Failed") # Error: Use "raise ... from e"
# B905: zip() without strict parameter
zip(list1, list2) # Error: Use zip(list1, list2, strict=True)
# B906: visit_ methods without isinstance check
def visit_Name(self, node): # Error: Add isinstance(node, ast.Name) check
pass
# B907: JSON loads without secure defaults
json.loads(data) # Error: Consider security implications
# B908: Context manager protocols
with context_manager() as ctx:
pass # Various context manager usage patterns
# B909: Mutation during iteration
for item in items:
items.remove(item) # Error: Modifying collection during iteration
# B910: Counter() instead of defaultdict(int)
defaultdict(int) # Error: Use Counter() for better memory efficiency
# B911: itertools.batched() without explicit strict parameter
itertools.batched(data, 3) # Error: Add strict=True parameter# B950: Line too long
very_long_line = "This line exceeds the configured maximum length and will trigger B950" # Error if > max_line_lengthAccess to package version information:
__version__: str # Package version string (e.g., "24.12.12")The BugBearVisitor class handles the actual AST traversal and error detection:
class BugBearVisitor:
"""
AST visitor that traverses code and detects rule violations.
Attributes:
filename (str): Name of file being analyzed
lines (List[str]): Source code lines
b008_b039_extend_immutable_calls (set): Extended immutable calls configuration
b902_classmethod_decorators (set): Classmethod decorators configuration
node_window (list): Recent AST nodes for context
errors (list): Collected error instances
contexts (list): Current context stack
b040_caught_exception: Exception context for B040 rule
"""
def visit(self, node):
"""Visit an AST node and apply bugbear rules."""
def generic_visit(self, node):
"""Generic visit implementation for unhandled node types."""Public utility functions for AST analysis:
def compose_call_path(node):
"""
Compose call path from AST call node.
Args:
node: AST node (Call, Attribute, or Name)
Yields:
str: Components of the call path
Example:
For `foo.bar.baz()`, yields: "foo", "bar", "baz"
"""
def is_name(node: ast.expr, name: str) -> bool:
"""
Check if AST node represents a specific name.
Args:
node: AST expression node to check
name: Name to match (supports dotted names like "typing.Generator")
Returns:
bool: True if node matches the given name
Example:
is_name(node, "typing.Generator") matches typing.Generator references
"""All errors follow a consistent format:
{filename}:{line}:{column} {code} {message}Examples:
example.py:5:8 B001 Do not use bare `except:`, it also catches unexpected events
example.py:12:4 B006 Do not use mutable data structures for argument defaults
example.py:20:1 B950 line too long (95 > 79 characters)repos:
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear][testenv:lint]
deps =
flake8
flake8-bugbear
commands = flake8 src tests- name: Run flake8
run: |
pip install flake8 flake8-bugbear
flake8 .