Ruff's S Rules Catch the Key. They Do Not Read the PR

Real Python's Sep 16 review workflow wants Ruff F/E/B/S/UP/DTZ and a 400-line attention cap. Talk Python still cites Ruff on 170k lines and CPython in 0.3s. Speed is not a review.

Real Python published a 33-minute tutorial on September 16 about reviewing agent code. The useful number is not the quiz at the bottom. It is four hundred lines, or about an hour, before most people lose focus. Talk Python To Me episode 563, September 17, is a Rust-for-Python-devs chat that still cannot stop talking about Ruff: 170,000 lines of the Talk Python Training app, four errors before Michael Kennedy could blink, CPython from scratch in 0.3 seconds.

Those two dates are a trap. The fast linter is step 2 in Leodanis Pozo Ramos’s five-step workflow. It is not the review. If you merge because Ruff was quiet, you skipped made-up APIs, swallowed errors, and the architecture that does not fit the repo.

We already did the modern toolchain: Ruff, uv, ty and 3.15rc1 freezing the ABI while Ruff rewrites except. This week is the human bottleneck, with a pyproject snippet attached.

The agent is faster than you. That is the bug.

Ramos says reviewing an agent’s PR is the same job as reviewing a teammate’s: does it do what it should, is it correct, secure, maintainable. What changed is volume and speed. The agent writes faster than you can read. You are the bottleneck. Most people fade after about 400 lines or an hour.

The mistake mix changes too. Logic that looks right and is wrong. Imports that do not exist. Edge cases skipped. Code that looks fine at a glance. That is why the workflow is not “read top to bottom.”

Five steps, two loops: understand intent, run automated checks, read with a plan starting at the trickiest parts, hunt the agent-specific failure modes, then fix and verify. A false alarm sends you back to reading. A failed fix sends you back to the checkers.

Prerequisites in the tutorial: you already read Python, you have used an agent (Claude Code, GitHub Copilot CLI, Antigravity CLI, OpenCode), you can debug, you have seen a type checker, you can read a git diff. Practice on a real agent PR. Use the project’s Python. For a new project he is fine with 3.14.

That last line is a version pin, not a vibe. If the agent wrote 3.14 syntax into a 3.11 fleet, Ruff UP rules will not save you by themselves. Intent first.

What the pyproject actually turns on

The minimal lint set Ramos wants for AI-generated code:

[tool.ruff.lint]
select = ["F", "E", "B", "S", "UP", "DTZ"]

[tool.mypy]
strict = true

[tool.coverage.run]
branch = true

F and E are the obvious ones. B is bugbear. S is security, which is how a hardcoded SECRET_KEY = "sk-liv...cdef" in auth.py gets caught. Bandit catches the same key on defaults. The fix is an environment variable or a .env file, not a comment. If it already hit git history, rotate it. The tutorial is explicit. History still has the string.

UP and DTZ are the training-data fossils: datetime.utcnow(), typing.List. Models regurgitate them. pip-audit for vulnerable dependencies. pylint duplicate-code across every module in one run, because it only compares files it sees together. radon if you want a complexity number. Do not run every linter on earth.

He will let you swap mypy for ty if you want the faster checker from the Ruff people. uv for a dev dependency group so the scanners are not in the runtime extra. Dev tools stay out of the app’s requirements. That is boring and it is the part agents skip when they write a README that pip-installs Bandit into production.

The sample auth.py key is written like a live Stripe-style secret. That is not a hypothetical. Agents copy .env.example into the module because the prompt said “add authentication.” S rules are how you find it in the PR instead of in a leak tweet. If your CI only runs Ruff F and E, you opted out of the one select list Ramos bothered to print.

Talk Python’s Christopher Trudeau line is why Ruff exists in this conversation: old Python tools were written in Python and slow in a way nobody noticed because you went for coffee. A 10 percent speedup is invisible. Multiple zeros and the workflow changes. Kennedy thought Ruff had aborted on 170k lines because four errors appeared immediately. It had not aborted. CPython linted from scratch in 0.3 seconds.

Use that speed to run S and UP on every agent PR. Do not use it as a substitute for step 3. If Ruff returns in 0.3 seconds and you spend zero minutes on intent, you used the wrong half of the week.

Read the scary parts first

Step 3 in the tutorial is not “skim the diff.” It is read with a plan, not top to bottom. Start where the risk is. Architecture and whether the change fits the modules you already have. Logic bugs. Missing edge cases and errors that get swallowed. Security. Made-up APIs and packages. Performance traps. Async and concurrency. Maintainability.

Made-up APIs are the agent special. The import works in the model’s head. pip install fails on your machine, or worse, installs a typosquat. Ramos puts that in the hunt list because a green Ruff F pass does not mean the package exists on PyPI. F catches undefined names in the file. It does not catch a real-looking library that should not be a dependency.

We already wrote code review in the AI era and a generic Python review checklist. The new instruction is operational: 400 lines, then stop. Split the PR. Do not hero-read 2,000 lines of generated glue because the model was cheap.

Confirm each problem by running the code before you fix it. That is the loop. Agents will “fix” a test by deleting the assertion. If you only read the second diff, you blessed it.

Ramos also wants you to understand intent before the linter. An agent that “adds caching” may have introduced a global dict that races in async. Ruff will format it. mypy strict may even like the types. The race is in step 4, concurrency, which is why he lists it after security and fake APIs. Order is the point. People start at style.

gh pr diff or git diff locally if you hate the browser. Same bytes.

Rust speed is a linter story, not a rewrite

Episode 563 is titled for Python developers picking up Rust. The clip that matters for this site is still the toolchain: Ruff, uv, ty, Pydantic, Polars, Granian. Ownership is the Rust idea Trudeau wants you to learn. You do not need ownership to merge an agent PR. You need S rules and a human who still checks whether sk-liv landed in auth.py.

If your team is rewriting hot paths in Rust because Kennedy’s 0.3 second demo felt like destiny, you are on a different project. This week’s tutorial is about not merging hallucinated packages. Keep them separate. We already covered ty and Pyrefly versus slow mypy. ty in Ramos’s prereqs is a swap for mypy, not a license to skip tests.

Branch coverage is on in the sample pyproject. Agents write the happy path. coverage run with branch = true is how you see the except Exception: pass they left in the parser. Ruff B and E will not always catch a swallowed error that is syntactically pretty.

Ramos’s FAQ-length tutorial is 33 minutes because the hunt list is long. You will not remember all of it. Remember three: fake packages, secrets, swallowed errors. The rest of step 4 is extra credit once those three are clean. Performance traps and “maintainability” are how tutorials get to 33 minutes. They are real. They are not why the key hit git.

If the agent opened a PR titled “refactor,” read the diff stats before the prose. A refactor that touches auth.py is an auth review. Rename it in your head.

A review card for this week

Do not adopt a five-step poster. Adopt four commands and one personal cap.

One: write the intent in the PR body before you generate. Ramos’s step 1. If the agent opened the PR, you write it. If you cannot say what it should do, you cannot review it.

Two: Ruff with F, E, B, S, UP, DTZ. mypy strict or ty. pip-audit. Bandit if S is not enough for you. Rotate anything S already found in git.

Three: read 400 lines of the highest-risk files, not the tests the agent wrote to flatter itself. Hit made-up imports with an actual install. Hit async with a test that interleaves.

Four: run the suite. If the fix fails, go back to the checkers, not to another generate-and-hope.

Kennedy’s 0.3 seconds is the budget for step two. Ramos’s hour is the budget for step three. If your agent dumped 3,000 lines, that is not one PR. That is seven reviews you pretended were one.

The 400-line cap is also a staffing rule. If two people review, that is 800 lines of attention, not two rubber stamps on the same 3,000-line glue dump. Split by directory. One person takes auth and anything that touches secrets. The other takes the generated tests. Do not both skip auth because Ruff was green.

Duplicate-code in pylint is the agent paste special. Ramos wants every module in one invocation. If you only lint the new file, you will miss the function the model copied from utils.py and renamed. radon is optional. Duplication is not.

The secret key example is the whole article in one file. Ruff S sees it. Bandit sees it. Git history keeps it if you already pushed. The review that matters happened before merge, or you are in rotation theater.

Talk Python’s show notes pile Ruff next to uv, ty, Pydantic, Polars, Granian. That is a shopping list for 2026 Python, not a merge checklist. Granian will not tell you the agent invented from acme.auth import rotate_keys and PyPI has no such module. pip install will. Do that install in CI, not in your head.

Save the Rust episode for the weekend. Merge the PR after the 400-line pass, not after the linter emoji.

Python 3.14 as Ramos’s default for new work is a reminder to pin the interpreter in CI to the same version the agent used. A 3.11 box will not catch 3.14-only syntax until deploy. The tutorial’s “use the project’s Python” is the whole version story. Write it in the workflow file, not in the PR comment.

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.