I ran your latest PR through Ruff before the standup. Forty-three violations. Eight of them are not style.
Forty-three. The CI did not catch them — we do not have Ruff there yet. But the eight that are not style — what are they?
A bare except: on line 91 that swallows every exception including KeyboardInterrupt and SystemExit. A from utils import * that makes it impossible to know where any name comes from. A mutable default argument — items=[] in the helper you wrote Thursday. And three variables assigned but never read.
Those are not style violations. The bare except is a real bug. The star import is a real dependency problem. The mutable default is the load-bearing wall. Those are logic errors hiding in syntax.
Now you understand what a linter actually is. The style checks are the building inspector measuring door widths. The real value is when the inspector taps the wall and says "this is load-bearing." Ruff catches both. The bare except is E722 — an error because it is nearly always wrong. A bare except catches BaseException, which includes KeyboardInterrupt. Your loop cannot be stopped with Ctrl-C if a bare except is in the call stack. The star import is F403. If utils adds a new name that collides with one of yours, you get a silent override with no error.
I have written bare except in every retry loop I have ever written. My reasoning was "catch everything so the loop does not die." I did not know I was catching Ctrl-C.
That is exactly the failure mode. A background worker with a bare except receives SIGINT, Python raises KeyboardInterrupt, the except catches it, the loop continues. You restart the service from outside. The loop is still running underneath. The fix is always specific: except Exception: instead of except:, because Exception does not include KeyboardInterrupt. If you need to handle keyboard interrupts, catch them explicitly in a separate clause.
How does Ruff catch the mutable default without running the code? It does not know what [] evaluates to at runtime.
Ruff reads the AST — the abstract syntax tree. It does not execute anything. It sees that the default value expression in the function signature is a list literal [], a dict literal {}, or a set() call. The literal form is what most developers write by accident, and that is what Ruff catches. The fix it suggests — items=None with if items is None: items = [] inside the body — is always correct.
What does the integration workflow look like? I have been running linters as an afterthought — fix violations before pushing.
Three layers. Editor: Ruff's VS Code extension flags violations as you type, the same way a type error underlines in a typed language. Pre-commit hook: violations block the commit. CI: violations block the merge. Here is the configuration that activates the categories that catch real bugs:
# pyproject.toml
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes — undefined names, unused imports
"B", # bugbear — mutable defaults, bare except
"I", # isort — import ordering
]The F and B rules are the ones that catch production bugs. E and I keep the codebase readable. By the time code reaches a reviewer, Ruff has already run three times.
I have been doing this in reverse. Write code, push, run the linter on the PR, fix in follow-up commits. My git history has "fix linting," "actually fix linting," "one more lint fix." I've been doing the CI layer manually.
Add ruff check --fix . to your pre-commit config tonight. That single command auto-fixes import ordering, deprecated syntax, unnecessary patterns. What it cannot fix — bare except, mutable default, star import — are the violations that require a decision. The linter pre-sorts for you: trivial fixes on one side, decisions on the other.
So the workflow is: ruff check --fix closes the automatable violations first, then I review what remains. What remains is always a violation that requires understanding the intent — which exception to actually catch, what default value to use instead of the mutable. The linter does the triage.
That is exactly how to work with it. The violations that survive --fix are almost always design decisions made too casually the first time. Running the linter is not a chore — it is clearing a queue of deferred decisions.
Ruff is not a style checker. It is a static analysis tool that implements rule sets from flake8, pylint, pyflakes, isort, pyupgrade, and flake8-bugbear — reimplemented in Rust for speed. Understanding what it can and cannot catch clarifies when to trust it and when to supplement it.
String-based vs. AST-based checks. Some violations can be detected by scanning the source text as strings — bare except: appears as a pattern on a line, wildcard imports match from X import *. These checks are fast and always run, even on code with syntax errors. Other violations require a parsed AST: mutable default arguments live in ast.FunctionDef.args.defaults, unused variables require comparing ast.Store contexts against ast.Load contexts in the same scope. If the code has a syntax error, AST-based checks are skipped.
The F rules are semantic, not syntactic. The F rule category (pyflakes) implements checks that require understanding Python's name resolution rules. F821 (undefined name) walks the AST, builds a symbol table for each scope, and checks that every Load reference has a corresponding Store or Import in an enclosing scope. F841 (unused variable) does the inverse — finds Store bindings with no corresponding Load in the same scope. These are the checks that catch real bugs, not style issues.
The B rules catch known footguns. The B category (flake8-bugbear) targets patterns that are legal Python but almost always bugs in practice. B006 (mutable default argument) checks function default values for list literals, dict literals, and set() calls. B007 catches loop variables used only in the loop header without being used in the body. B017 catches assertRaises(Exception) calls that are too broad. These rules encode years of "this pattern caused a production incident" knowledge into automated checks.
Auto-fix scope and safety. ruff check --fix applies "safe" fixes — transformations guaranteed not to change program behavior. Import reordering is safe because Python evaluates imports in order but the order rarely matters for behavior. Removing unused imports is safe if the import has no side effects (Ruff assumes it does not unless it recognizes the pattern). --unsafe-fixes applies transformations that change semantics: rewriting Optional[str] to str | None changes type annotations but not runtime behavior in most cases. Understanding the distinction helps you decide when to apply fixes automatically and when to review them.
Sign up to write and run code in this lesson.
I ran your latest PR through Ruff before the standup. Forty-three violations. Eight of them are not style.
Forty-three. The CI did not catch them — we do not have Ruff there yet. But the eight that are not style — what are they?
A bare except: on line 91 that swallows every exception including KeyboardInterrupt and SystemExit. A from utils import * that makes it impossible to know where any name comes from. A mutable default argument — items=[] in the helper you wrote Thursday. And three variables assigned but never read.
Those are not style violations. The bare except is a real bug. The star import is a real dependency problem. The mutable default is the load-bearing wall. Those are logic errors hiding in syntax.
Now you understand what a linter actually is. The style checks are the building inspector measuring door widths. The real value is when the inspector taps the wall and says "this is load-bearing." Ruff catches both. The bare except is E722 — an error because it is nearly always wrong. A bare except catches BaseException, which includes KeyboardInterrupt. Your loop cannot be stopped with Ctrl-C if a bare except is in the call stack. The star import is F403. If utils adds a new name that collides with one of yours, you get a silent override with no error.
I have written bare except in every retry loop I have ever written. My reasoning was "catch everything so the loop does not die." I did not know I was catching Ctrl-C.
That is exactly the failure mode. A background worker with a bare except receives SIGINT, Python raises KeyboardInterrupt, the except catches it, the loop continues. You restart the service from outside. The loop is still running underneath. The fix is always specific: except Exception: instead of except:, because Exception does not include KeyboardInterrupt. If you need to handle keyboard interrupts, catch them explicitly in a separate clause.
How does Ruff catch the mutable default without running the code? It does not know what [] evaluates to at runtime.
Ruff reads the AST — the abstract syntax tree. It does not execute anything. It sees that the default value expression in the function signature is a list literal [], a dict literal {}, or a set() call. The literal form is what most developers write by accident, and that is what Ruff catches. The fix it suggests — items=None with if items is None: items = [] inside the body — is always correct.
What does the integration workflow look like? I have been running linters as an afterthought — fix violations before pushing.
Three layers. Editor: Ruff's VS Code extension flags violations as you type, the same way a type error underlines in a typed language. Pre-commit hook: violations block the commit. CI: violations block the merge. Here is the configuration that activates the categories that catch real bugs:
# pyproject.toml
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes — undefined names, unused imports
"B", # bugbear — mutable defaults, bare except
"I", # isort — import ordering
]The F and B rules are the ones that catch production bugs. E and I keep the codebase readable. By the time code reaches a reviewer, Ruff has already run three times.
I have been doing this in reverse. Write code, push, run the linter on the PR, fix in follow-up commits. My git history has "fix linting," "actually fix linting," "one more lint fix." I've been doing the CI layer manually.
Add ruff check --fix . to your pre-commit config tonight. That single command auto-fixes import ordering, deprecated syntax, unnecessary patterns. What it cannot fix — bare except, mutable default, star import — are the violations that require a decision. The linter pre-sorts for you: trivial fixes on one side, decisions on the other.
So the workflow is: ruff check --fix closes the automatable violations first, then I review what remains. What remains is always a violation that requires understanding the intent — which exception to actually catch, what default value to use instead of the mutable. The linter does the triage.
That is exactly how to work with it. The violations that survive --fix are almost always design decisions made too casually the first time. Running the linter is not a chore — it is clearing a queue of deferred decisions.
Ruff is not a style checker. It is a static analysis tool that implements rule sets from flake8, pylint, pyflakes, isort, pyupgrade, and flake8-bugbear — reimplemented in Rust for speed. Understanding what it can and cannot catch clarifies when to trust it and when to supplement it.
String-based vs. AST-based checks. Some violations can be detected by scanning the source text as strings — bare except: appears as a pattern on a line, wildcard imports match from X import *. These checks are fast and always run, even on code with syntax errors. Other violations require a parsed AST: mutable default arguments live in ast.FunctionDef.args.defaults, unused variables require comparing ast.Store contexts against ast.Load contexts in the same scope. If the code has a syntax error, AST-based checks are skipped.
The F rules are semantic, not syntactic. The F rule category (pyflakes) implements checks that require understanding Python's name resolution rules. F821 (undefined name) walks the AST, builds a symbol table for each scope, and checks that every Load reference has a corresponding Store or Import in an enclosing scope. F841 (unused variable) does the inverse — finds Store bindings with no corresponding Load in the same scope. These are the checks that catch real bugs, not style issues.
The B rules catch known footguns. The B category (flake8-bugbear) targets patterns that are legal Python but almost always bugs in practice. B006 (mutable default argument) checks function default values for list literals, dict literals, and set() calls. B007 catches loop variables used only in the loop header without being used in the body. B017 catches assertRaises(Exception) calls that are too broad. These rules encode years of "this pattern caused a production incident" knowledge into automated checks.
Auto-fix scope and safety. ruff check --fix applies "safe" fixes — transformations guaranteed not to change program behavior. Import reordering is safe because Python evaluates imports in order but the order rarely matters for behavior. Removing unused imports is safe if the import has no side effects (Ruff assumes it does not unless it recognizes the pattern). --unsafe-fixes applies transformations that change semantics: rewriting Optional[str] to str | None changes type annotations but not runtime behavior in most cases. Understanding the distinction helps you decide when to apply fixes automatically and when to review them.