Learning Rust? I co-run a 6-week Python to Rust cohort where you build a performant JSON parser with PyO3 bindings.
Cal Newport's On AI Coding and Its Discontents lands on an uncomfortable point: the speed AI gives you is paid for with the thinking that made us good engineers in the first place.
Read code instead of writing it and you get passive recognition where you used to have an active model of the code.
The vendors sell one answer to this: more AI. The nostalgic answer is the opposite: go back to writing everything by hand. I am searching for a midway.
No right answer, but at least a direction to pursue #
When I posted about this recently, I offered 3 options: write more code manually, double down on AI, or find some middle with more friction. I got some valuable comments from developers picking the middle. And that's where I've landed too.
The more I read and think about it, the more the middle stops looking like a compromise. It seems the way forward, but only if you specify what friction to keep.
Skill atrophy isn't caused by using AI per se. It's caused by delegating the part that builds your internal model.
I've argued that the friction is where the learning lives, and nothing about agents changes that. What changes is that it's now easier to bypass friction and it's on us to keep it in.
It helps to separate two problems here, because AI creates both and they aren't the same. One is skill atrophy: stop doing the thinking and your mental model fades. The other is correctness: AI writes plausible code that can be subtly wrong.
Let's look at each in turn.
Do you 'own' the code? #
A sharp reply came from someone on Bluesky:
agents draft, but re-derive the hard path before merge. If you can't explain a weird branch without re-reading the whole file, you rewrote too little.
Not all code is created equal. The boundary conditions, error paths, and testing assumptions are where we as engineers pull our weight.
For every diff, can you explain the change? What are the edge cases and risks? In practice I find that this gets easier when you scope the agent to narrow, well-defined tasks instead of blanket features. Small tasks keep you in the loop, stop the agent from drifting, and prevent giant code reviews nobody wants to read.
I also like how Corey Schafer summarized how he keeps the friction:
- Use detailed prompts, not one-liners: spec the tools, architecture, and reuse saved research prompts that force citing official docs.
- Review everything: read and understand all generated code like a junior's PR you'd never merge blindly.
- Interrogate unfamiliar code / concepts: make the AI explain things in plain English, pull docs, and justify the approach and trade-offs.
- Push back: offer your own approach, ask if the AI's is better/worse/different, and fold what you learn into your own toolkit.
Push the rest into the system #
Those four habits are all manual. They keep your judgment in the loop, but they run on your attention, and attention is finite. You can't re-read every branch on every diff forever. So whatever those habits can't cover by hand, you push into the system and let it enforce the rest automatically.
A commenter on Threads put it well:
the real fix is correctness by design, stronger types, compile-time assertions, more "faff in the way." We never fully adopted that stuff because it takes patience humans run out of. AI doesn't. It learns your constraints once, then works inside them on every diff after.
And on Fosstodon:
I feel like all these systems have too many surprises, and making invalid states illegal is the best defensive alternative to not using them, which would be the first choice. Once you get guarantees, if you can type faster with gen ai than your fingers, maybe thats worth the squeeze depending on your morals.
That's why Rust is so appealing to me. Making invalid states unrepresentable and the agent has to comply with this. I discussed this here.
Python gives you less strict guarantees, but we can approximate it. Your type checker (ty, pyrefly, mypy) catches the class of type errors the model would otherwise slip past you. Pydantic models validate at the boundary, so bad data fails loudly instead of drifting three functions deep. And well-thought out test cases can challenge plausible looking code. And for AI generated tests you can use mutation testing (an article on this soon here ...)
So balancing responsible AI use has two sides. Re-derive the decisions that matter, which keeps your judgment sharp. And encode stricter rules to keep your agent accountable to higher standards. One protects your mental model, the other protects the code base. Both protect peace of mind.
As said before: the tooling / way of working has changed, but AI doesn't change what software engineering is; we can automate some of the boring parts, but the majority of the work (including the code-producing part) is a lot of back-and-forth and applying experience and judgment.
Keep reading #
How to Keep Your Developer Instincts When AI Writes the CodeAI Is an Accelerator, Not a CompassFrom 1,069 to 156 LOC: Design Over Code
Newport's worry is right: outsourced thinking atrophies. The escape isn't more or less AI. It's identifying, every time you build software, the small set of decisions you refuse to let the model make for you.
Can you explain each change? Do you truly own it? Did you keep making the hard decisions yourself? And are strict guardrails in place to protect the codebase when AI writes more of the code?
Reach out to me if you want to chat about this. It feels a bit like the wild west right now, and nobody has final answers. I'm really keen to hear how you're navigating the fast pace of change our industry is going through.