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.