IDE Warnings & Linting
Linting tools analyze your code for potential bugs, style issues, and anti-patterns without running it. They catch problems that tests might miss, enforce consistency across a codebase, and help you write better code from the start.
Why Linting Matters
Linters catch real bugs before they reach production:
- Unused imports that slow startup and confuse readers
- Undefined variables from typos that would crash at runtime
- Mutable default arguments that cause subtle data corruption
- Shadowed built-in names that break standard library functions
- Type errors that would only surface in specific code paths
Even experienced developers benefit from linting. It acts as an automated code reviewer that never gets tired and never misses the obvious.
Popular Linting Tools
| Tool | Purpose | Speed | Notes |
|---|---|---|---|
| ruff | Linter + formatter | Very fast | Written in Rust, replaces flake8 + isort + many plugins |
| flake8 | Style + logical errors | Fast | Mature ecosystem with many plugins |
| pylint | Deep analysis | Slow | Catches more issues but more false positives |
| mypy | Static type checking | Medium | Checks type annotations for correctness |
| pyright | Static type checking | Fast | Used by VS Code's Pylance extension |
For new projects, ruff is the recommended starting point. It is extremely fast and covers the rules from flake8, isort, pyflakes, and many other tools in a single binary.
Common Warnings and What They Mean
F401: Unused Import
An imported module or name is never used in the file.
# Warning: F401 — 'os' imported but unused
import os
import sys
def main():
print(sys.version)
Fix: Remove the unused import, or if it is intentionally re-exported, mark it explicitly.
import sys
def main():
print(sys.version)
Click "Run" to execute your codeF841: Unused Variable
A variable is assigned but never read.
# Warning: F841 — local variable 'result' is assigned but never used
def process():
result = expensive_calculation()
return True # Oops, forgot to use result!
Fix: Either use the variable or prefix with _ to indicate it is intentionally unused.
def process():
result = expensive_calculation()
return result
# If the return value is intentionally ignored:
_ = some_function_with_side_effects()
A001: Shadowed Built-in Name
Using a variable name that shadows a Python built-in function or type.
# Warning: A001 — variable 'list' is shadowing a Python builtin
list = [1, 2, 3] # Shadows the built-in list() function!
id = get_user_id() # Shadows the built-in id() function!
type = "admin" # Shadows the built-in type() function!
input = get_form_data() # Shadows the built-in input() function!
Fix: Choose a more descriptive name.
items = [1, 2, 3]
user_id = get_user_id()
user_type = "admin"
form_input = get_form_data()
Click "Run" to execute your codePLR0913: Too Many Arguments
A function has too many parameters (default threshold: 5).
# Warning: PLR0913 — too many arguments (8 > 5)
def create_user(name, email, age, address, phone, role, department, manager):
...
Fix: Group related parameters into a data class or dictionary.
from dataclasses import dataclass
@dataclass
class UserInfo:
name: str
email: str
age: int
address: str
phone: str
@dataclass
class OrgInfo:
role: str
department: str
manager: str
def create_user(user: UserInfo, org: OrgInfo):
...
Click "Run" to execute your codeMissing Return Type Annotation
Functions lack type annotations, making it harder for type checkers and IDEs to catch bugs.
# Warning: missing return type annotation
def add(a, b):
return a + b
# Fixed: add type annotations
def add(a: int, b: int) -> int:
return a + b
Type annotations are checked by mypy and pyright, not by ruff or flake8. They help catch type-related bugs without running the code.
E501: Line Too Long
A line exceeds the maximum length (default: 79 for PEP 8, commonly set to 88 or 120).
# Warning: E501 — line too long (95 > 88 characters)
result = some_function(first_argument, second_argument, third_argument, fourth_argument, fifth_argument)
# Fix: break the line
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument,
fifth_argument,
)
E722: Bare except
Using except: without specifying an exception type catches everything, including KeyboardInterrupt and SystemExit.
# Warning: E722 — do not use bare 'except'
try:
risky_operation()
except:
pass
# Fix: catch specific exceptions
try:
risky_operation()
except (ValueError, TypeError) as e:
handle_error(e)
B006: Mutable Default Argument
A mutable object (list, dict, set) is used as a function default argument.
# Warning: B006 — mutable default argument
def add_item(item, items=[]):
items.append(item)
return items
# Fix: use None as default
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
I001: Import Order
Imports are not sorted according to the standard convention (stdlib, third-party, local).
# Warning: I001 — import block is unsorted
import requests
import os
from myapp import utils
import sys
# Fix: sort by category, then alphabetically
import os
import sys
import requests
from myapp import utils
Click "Run" to execute your codeOOP-Related Warnings
Linters also catch common object-oriented programming issues.
Too Many Ancestors (Inheritance Depth)
Deep inheritance hierarchies are hard to understand and maintain.
# Warning: R0901 — too many ancestors (8/7)
class A: ...
class B(A): ...
class C(B): ...
class D(C): ...
class E(D): ...
class F(E): ...
class G(F): ...
class H(G): ... # 8 levels deep!
Fix: Flatten the hierarchy using composition or mixins.
Too Many Instance Attributes
A class with too many instance attributes is probably doing too much.
# Warning: R0902 — too many instance attributes (12/7)
class UserProfile:
def __init__(self):
self.name = ""
self.email = ""
self.phone = ""
self.address = ""
self.city = ""
self.state = ""
self.zip_code = ""
self.country = ""
self.bio = ""
self.avatar_url = ""
self.created_at = None
self.updated_at = None
Fix: Split into smaller, focused classes.
@dataclass
class Address:
street: str
city: str
state: str
zip_code: str
country: str
@dataclass
class UserProfile:
name: str
email: str
phone: str
address: Address
bio: str
avatar_url: str
Abstract Method Not Overridden
When a class inherits from an ABC but forgets to implement an abstract method.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
# Warning: class Circle does not override abstract method 'perimeter'
class Circle(Shape):
def area(self):
return 3.14 * self.r ** 2
# Forgot perimeter()!
Python will raise TypeError at instantiation time, but linters catch this at edit time.
Click "Run" to execute your codeHow to Configure Linting
Using pyproject.toml (Recommended)
The pyproject.toml file is the modern standard for Python project configuration. Most tools support it.
# pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"A", # flake8-builtins
"UP", # pyupgrade
"PLR", # pylint refactoring
]
ignore = [
"E501", # line too long (handled by formatter)
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["PLR0913"] # Allow many arguments in tests
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
Using ruff.toml
If you prefer a standalone ruff configuration:
# ruff.toml
line-length = 88
target-version = "py312"
[lint]
select = ["E", "F", "I", "B", "A", "UP"]
ignore = ["E501"]
Flake8 Configuration
Flake8 uses .flake8 or setup.cfg:
# .flake8
[flake8]
max-line-length = 88
extend-ignore = E203, E501
per-file-ignores =
tests/*:S101
Click "Run" to execute your codeWhen to Suppress Warnings
Sometimes a warning is wrong for your specific situation. You can suppress it, but do so sparingly and always include a reason.
# noqa for Ruff and Flake8
# Suppress a specific rule on one line
from mylib import helper # noqa: F401 — re-exported for public API
# Suppress all warnings on a line (avoid this)
something_weird() # noqa
# type: ignore for Mypy
# Suppress type checking for a specific line
result = some_dynamic_function() # type: ignore[no-untyped-call]
When NOT to Suppress
Do not suppress warnings that indicate real bugs:
- Mutable default arguments (B006) -- fix the code instead
- Bare except (E722) -- catch specific exceptions
- Undefined names (F821) -- fix the typo or import
Do not suppress broadly:
- Never use
# noqawithout a specific code (e.g.,# noqa: F401) - Never add entire files to ignore lists without good reason
- Never disable important rules globally because one file has violations
# BAD: suppresses everything, hides real issues
do_something() # noqa
# GOOD: suppresses specific rule with explanation
do_something() # noqa: S101 — assert used intentionally in test helper
Click "Run" to execute your codeSetting Up Pre-Commit Hooks
Pre-commit hooks run linting automatically before every commit, preventing bad code from entering the repository.
Installing pre-commit
pip install pre-commit
Creating .pre-commit-config.yaml
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: []
Activating the Hooks
# Install the hooks (run once after cloning)
pre-commit install
# Run against all files manually
pre-commit run --all-files
# Now every git commit will run the hooks automatically
git commit -m "My changes" # ruff and mypy run first!
What Happens When a Hook Fails
If a hook finds issues:
- The commit is blocked
- Auto-fixable issues (like import sorting) are fixed automatically
- You review the changes, stage them, and commit again
This ensures every commit in the repository passes linting.
Click "Run" to execute your codeQuick Reference: Warning Codes
| Code | Tool | Description | Fix |
|---|---|---|---|
| F401 | pyflakes | Unused import | Remove it |
| F841 | pyflakes | Unused variable | Use it or prefix with _ |
| A001 | flake8-builtins | Shadows built-in | Rename the variable |
| PLR0913 | pylint | Too many arguments | Group into dataclass |
| E501 | pycodestyle | Line too long | Break the line |
| E722 | pycodestyle | Bare except | Catch specific exceptions |
| B006 | flake8-bugbear | Mutable default arg | Use None as default |
| I001 | isort | Unsorted imports | Sort or auto-fix |
| R0901 | pylint | Too many ancestors | Flatten hierarchy |
| R0902 | pylint | Too many attributes | Split class |
Recommended Setup for New Projects
For a new Python project, this is a solid starting configuration:
# pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"A", # flake8-builtins
"UP", # pyupgrade
"SIM", # flake8-simplify
"PLR", # pylint refactoring
]
[tool.ruff.format]
quote-style = "double"
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
This gives you a strong set of checks without being overwhelming. You can add more rules as your project matures.