Heed Static Analysis Warnings

Your tools can find many of these problems before you run the code. Read what they say and fix it.

Why this matters

A static analysis tool reads your source code without running it and reports things that look wrong: a variable you assigned but never used, a name you typed that does not exist, a bare except:, a name you misspelled on one line. In compiled languages, the compiler does some of this and prints warnings. Python has no compiler step, so the job falls to separate tools, called linters, and to your IDE.

The principle says to heed the warnings. That is the hard part. Warnings are easy to ignore because the program still runs. But every warning is a place where the tool could not make sense of what you wrote, and that is usually a place where you did not either. An unused variable is often a typo that silently broke a calculation. An undefined name is a crash waiting for the one input that reaches that line. A bare except: is the swallowed error from the exception handling page.

Fixing warnings is cheap. The tool found the line for you. Ignoring them means finding the same problem later, at run time, with less information.

The rule: Run a linter on your code and keep the warning count at zero. Treat each warning as a question about your code that needs an answer: fix it, or understand exactly why it is safe and silence that one warning with a comment that says why.

Benefits

  • Typos, unused code, and undefined names are caught in seconds instead of in testing.
  • Several of the other principles in this lab have a matching warning. The linter enforces them for you.
  • Code that is warning-free is easier for teammates to read, because every line is there on purpose.
  • A clean baseline means a new warning stands out immediately.

Red flags (violations)

  • The Problems window in PyCharm shows dozens of items and nobody has looked at them.
  • Yellow or red underlines in the editor that have been there for weeks.
  • # noqa or # type: ignore comments with no reason given.
  • A linter that is installed but never run, or that is run only right before a submission.
  • Warnings fixed by deleting the check instead of fixing the cause.

Two tools you already have or can install in a minute

PyCharm’s built-in inspections

PyCharm analyzes your code as you type. A yellow underline is a warning. A red underline is an error. Hover over the underline to read the message, and press Alt+Enter (Option+Enter on macOS) to see suggested fixes.

To see every problem in the project at once, open the Problems tool window (View > Tool Windows > Problems) or run Code > Inspect Code... on the whole project. Work through the list.

Ruff from the command line

Ruff is a fast linter that checks for the most common mistakes. Install it into your project’s environment and run it on the project folder:

pip install ruff
ruff check .

A clean project prints All checks passed!. Otherwise it reports each problem with the file, the line, a code, the message, and the lines of code around it:

F841 Local variable `totl` is assigned to but never used
  --> inventory/storage.py:19:9
   |
17 |     total = 0
18 |     for name, quantity in items.items():
19 |         totl = total + quantity
   |         ^^^^
20 |     return total
   |
help: Remove assignment to unused variable `totl`

For a shorter list with one line per problem, add --output-format concise:

$ ruff check --output-format concise .
inventory/storage.py:1:8: F401 [*] `os` imported but unused
inventory/storage.py:2:8: F401 [*] `sys` imported but unused
inventory/storage.py:8:5: E722 Do not use bare `except`
inventory/storage.py:19:9: F841 Local variable `totl` is assigned to but never used
inventory/storage.py:20:12: F821 Undefined name `itmes`
Found 5 errors.
[*] 2 fixable with the `--fix` option.

The code tells you what kind of problem it is. F codes come from Pyflakes and are almost always real bugs: unused imports (F401), unused variables (F841), undefined names (F821). E codes are correctness and style rules from pycodestyle; E722, the bare except, is the one you will see most. You may also see I001, which means the imports at the top of the file are out of order. Ruff can fix that one for you.

ruff check --fix . repairs the problems marked [*], such as removing an unused import. Look at what it changed before you commit.

To see Ruff’s results inside PyCharm, install the Ruff plugin from Settings > Plugins. Then its warnings appear as underlines alongside PyCharm’s own.

Example 1 - A typo the linter catches and the tests might not

Problematic code

def total_quantity(items: dict) -> int:
    total = 0
    for name, quantity in items.items():
        totl = total + quantity
    return total

This runs. It returns 0 for every inventory. A test with an empty inventory passes. Ruff reports:

F841 Local variable `totl` is assigned to but never used

Fixed

def total_quantity(items: dict) -> int:
    total = 0
    for name, quantity in items.items():
        total = total + quantity
    return total

Why this is better: The warning pointed straight at the line. PyCharm shows the same thing as a gray totl with a hover message. Reading the warning took ten seconds; finding the bug from a wrong total somewhere in the program could have taken an hour.

Example 2 - Warnings that match the other principles

Problematic code

import os
import sys

def load_inventory(filename):
    try:
        with open(filename) as f:
            return parse(f)
    except:
        return {}

def find_item(items, name):
    try:
        return items[name]
    except:
        print("Not found")

Ruff reports (concise format):

inventory/storage.py:1:8: F401 [*] `os` imported but unused
inventory/storage.py:2:8: F401 [*] `sys` imported but unused
inventory/storage.py:8:5: E722 Do not use bare `except`
inventory/storage.py:16:5: E722 Do not use bare `except`

Fixed

def load_inventory(filename: str) -> dict:
    """Read the inventory file. Raises OSError if it cannot be opened."""
    with open(filename) as f:
        return parse(f)

def find_item(items: dict, name: str) -> int | None:
    """Return the quantity for name, or None if the item is not stocked."""
    return items.get(name)

Why this is better: The first bare except was swallowing every error, including a missing file, and returning an empty inventory as if nothing happened. The fix is the one from the exception handling page: let it raise. The second bare except caught KeyError and printed from inside a lookup function, which is a user-interface decision made in the wrong place; .get() returns None and lets the caller decide. The unused imports are gone, so a reader does not wonder what they were for.

How to apply this rule

  1. Run the linter before every commit. ruff check . takes under a second. Make it a habit, like saving the file.
  2. Read the message, then the line. The message says what the tool saw. The line shows why. Decide which one is wrong.
  3. Fix the cause. Rename the typo, delete the unused code, catch the specific exception. Do not just make the warning go away.
  4. Silence on purpose, with a reason. Rarely, a warning is wrong for your situation. Add # noqa: <code> to that one line with a comment explaining why. Never silence a whole file.
  5. Keep the count at zero. When the baseline is clean, a new warning is a new problem, and you see it immediately.

Knowledge Check

  • Ruff reports F841 Local variable 'reslt' is assigned to but never used. What is the most likely cause?
    1. Ruff is wrong; unused variables are fine
    2. A typo: the code assigned to reslt but later used result
    3. The variable is used in another file
    4. Python requires every variable to be returned
    Answer2 — a misspelled name on the assignment creates a new variable nobody reads, and the intended variable keeps its old value. The program runs, with the wrong answer.
  • Why does Python need a separate linter where a C or Java compiler would print warnings itself?
    1. Python has no compile step that checks the whole program before it runs
    2. Python code has no bugs
    3. Linters are only for style
    4. Python compilers are too slow
    Answer1 — Python checks a line when it runs it. A tool that reads the whole file ahead of time is the only way to find problems on lines that have not run yet.
  • A teammate adds # noqa to the end of every line Ruff complains about and commits. What is wrong with this?
    1. Nothing; the warnings are gone
    2. The problems are still in the code; only the messages were removed
    3. # noqa is not valid Python
    4. Ruff will refuse to run
    Answer2 — silencing a warning without fixing or understanding it keeps the bug and loses the reminder. A `noqa` is acceptable only for a single line, with a specific code and a reason.
  • Which warning points at a violation of the “handle exceptions at the lowest sensible level” principle?
    1. F401 'os' imported but unused
    2. I001 Import block is un-sorted or un-formatted
    3. E722 Do not use bare 'except'
    4. F841 Local variable assigned but never used
    Answer3 — a bare `except` catches everything, including errors the function cannot handle, which is the swallowing pattern that principle warns against.
Last modified October 27, 2026.