You got 47 linting warnings and you treated them as noise. I want to start there. What is your mental model of what a linter is doing?
Checking formatting. Spacing, line length, naming. Things that affect readability but not correctness. I have always thought of linters as tools for teams that want to enforce house style. The warnings are cosmetic.
That model is half right and half dangerous. PEP 8 naming and spacing rules — those are style. But ruff and mypy catch real bugs too. A mutable default argument is a shared-state bug the linter flags before any test catches it. A bare except swallows KeyboardInterrupt and SystemExit — that is a correctness bug. == None instead of is None fails for objects that define __eq__. These look exactly like cosmetic warnings in the dashboard. Same red badge. Same category. The skill is knowing which warnings are building code and which are paint.
So the building inspection analogy. Not every rule prevents a collapse, but they are all under the same category. I need to read the rule codes to know whether I am looking at a structural issue or a cosmetic one.
Exactly. Ruff uses Pyflakes rule codes for the structural issues — F-prefix for unused imports, undefined names, redefined variables. E-prefix is PEP 8 style. Once you know the taxonomy, triage is straightforward: fix F rules first, treat E rules as team contract, and know what you are accepting when you suppress a W rule.
I have been dismissing all of them equally. There are three functions in our codebase right now with mutable default arguments — I wrote all three. The linter has been flagging them for months. I read the warning, thought "cosmetic," and moved on. Those are real bugs.
Yes. And they are exactly the bugs that do not appear in testing — they only appear when someone calls the function twice in a row and the state from the first call is still in the default list from the second call. The linter caught them. The tests did not. That is the building inspector finding the load-bearing wall problem before the floor falls through.
This week is docstrings, type annotations, and anti-patterns on top of PEP 8 and ruff. What is the order I should care about these in? If I can only fix one category of problem first, which one is doing the most damage right now?
Fix the F-category rules first — they are real bugs. Then add type annotations to functions that cross module boundaries — that is where mypy catches the type errors that cost you debugging hours. Then naming and docstrings — dividends in code review time and in six months when someone else reads your code. PEP 8 formatting is last because ruff auto-fixes it.
The professional case for linters, type checkers, and documentation standards is not about consistency or aesthetics. It is about shifting error discovery left — finding bugs earlier in the development cycle, when they are cheap to fix, rather than later, when they are expensive.
A bug found in a code review costs minutes. A bug found in production costs hours, sometimes days, plus the cost of user impact, incident response, and trust. Code quality tools find a specific category of bugs — type errors, undefined names, shared mutable state, swallowed exceptions — that are invisible to tests because tests only find bugs on the paths the test author thought to exercise. A mutable default argument fails only when the same function is called twice with the default in the same process. Tests rarely do this unless someone specifically writes a test for it. The linter finds it on every run.
Type annotations and mypy extend this further. A function that takes a str and treats it as a float somewhere in its body will fail silently at every call site that passes a number-looking string and catastrophically at the call sites that do not. mypy finds this without running the code, at the cost of writing annotations that double as documentation. The annotation def process(amount: float) -> str tells the caller more than a docstring that says "processes an amount." It tells them exactly what type to pass and what type to expect back, and mypy enforces that contract across every call site.
Docstrings complete the picture. Code is read ten times more often than it is written. A function without a docstring that explains its contract — what the parameters mean, what edge cases it handles, what it raises — forces every reader to reverse-engineer that understanding from the implementation. A function with a complete Google-style docstring gives that understanding in ten seconds. At scale, across a codebase with hundreds of functions, the difference in maintenance cost is substantial.
The week you are starting is not about making your code look professional. It is about building the toolchain that catches bugs you cannot test for, enforces contracts you cannot review manually, and documents decisions you will not remember in six months. The goal is a codebase where ruff, mypy, and complete docstrings make it structurally harder to introduce certain classes of bugs.
Sign up to save your notes.
You got 47 linting warnings and you treated them as noise. I want to start there. What is your mental model of what a linter is doing?
Checking formatting. Spacing, line length, naming. Things that affect readability but not correctness. I have always thought of linters as tools for teams that want to enforce house style. The warnings are cosmetic.
That model is half right and half dangerous. PEP 8 naming and spacing rules — those are style. But ruff and mypy catch real bugs too. A mutable default argument is a shared-state bug the linter flags before any test catches it. A bare except swallows KeyboardInterrupt and SystemExit — that is a correctness bug. == None instead of is None fails for objects that define __eq__. These look exactly like cosmetic warnings in the dashboard. Same red badge. Same category. The skill is knowing which warnings are building code and which are paint.
So the building inspection analogy. Not every rule prevents a collapse, but they are all under the same category. I need to read the rule codes to know whether I am looking at a structural issue or a cosmetic one.
Exactly. Ruff uses Pyflakes rule codes for the structural issues — F-prefix for unused imports, undefined names, redefined variables. E-prefix is PEP 8 style. Once you know the taxonomy, triage is straightforward: fix F rules first, treat E rules as team contract, and know what you are accepting when you suppress a W rule.
I have been dismissing all of them equally. There are three functions in our codebase right now with mutable default arguments — I wrote all three. The linter has been flagging them for months. I read the warning, thought "cosmetic," and moved on. Those are real bugs.
Yes. And they are exactly the bugs that do not appear in testing — they only appear when someone calls the function twice in a row and the state from the first call is still in the default list from the second call. The linter caught them. The tests did not. That is the building inspector finding the load-bearing wall problem before the floor falls through.
This week is docstrings, type annotations, and anti-patterns on top of PEP 8 and ruff. What is the order I should care about these in? If I can only fix one category of problem first, which one is doing the most damage right now?
Fix the F-category rules first — they are real bugs. Then add type annotations to functions that cross module boundaries — that is where mypy catches the type errors that cost you debugging hours. Then naming and docstrings — dividends in code review time and in six months when someone else reads your code. PEP 8 formatting is last because ruff auto-fixes it.
The professional case for linters, type checkers, and documentation standards is not about consistency or aesthetics. It is about shifting error discovery left — finding bugs earlier in the development cycle, when they are cheap to fix, rather than later, when they are expensive.
A bug found in a code review costs minutes. A bug found in production costs hours, sometimes days, plus the cost of user impact, incident response, and trust. Code quality tools find a specific category of bugs — type errors, undefined names, shared mutable state, swallowed exceptions — that are invisible to tests because tests only find bugs on the paths the test author thought to exercise. A mutable default argument fails only when the same function is called twice with the default in the same process. Tests rarely do this unless someone specifically writes a test for it. The linter finds it on every run.
Type annotations and mypy extend this further. A function that takes a str and treats it as a float somewhere in its body will fail silently at every call site that passes a number-looking string and catastrophically at the call sites that do not. mypy finds this without running the code, at the cost of writing annotations that double as documentation. The annotation def process(amount: float) -> str tells the caller more than a docstring that says "processes an amount." It tells them exactly what type to pass and what type to expect back, and mypy enforces that contract across every call site.
Docstrings complete the picture. Code is read ten times more often than it is written. A function without a docstring that explains its contract — what the parameters mean, what edge cases it handles, what it raises — forces every reader to reverse-engineer that understanding from the implementation. A function with a complete Google-style docstring gives that understanding in ten seconds. At scale, across a codebase with hundreds of functions, the difference in maintenance cost is substantial.
The week you are starting is not about making your code look professional. It is about building the toolchain that catches bugs you cannot test for, enforces contracts you cannot review manually, and documents decisions you will not remember in six months. The goal is a codebase where ruff, mypy, and complete docstrings make it structurally harder to introduce certain classes of bugs.