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)

Python Playground
Output
Click "Run" to execute your code

F841: 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()

Python Playground
Output
Click "Run" to execute your code

PLR0913: 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):
    ...

Python Playground
Output
Click "Run" to execute your code

Missing 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

Python Playground
Output
Click "Run" to execute your code

OOP-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.

Python Playground
Output
Click "Run" to execute your code

How 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

Python Playground
Output
Click "Run" to execute your code

When 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 # noqa without 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

Python Playground
Output
Click "Run" to execute your code

Setting 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:

  1. The commit is blocked
  2. Auto-fixable issues (like import sorting) are fixed automatically
  3. You review the changes, stage them, and commit again

This ensures every commit in the repository passes linting.

Python Playground
Output
Click "Run" to execute your code

Quick 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.