Code should be self documenting. That sounds good, but what does it mean? It’s about making your code legible. Names of things, variable, method, class, package, library, executable, etc should mean something. What the code is doing should be obvious from reading. Encapsulation and decomposition helps a lot here.
Writing the code itself is a conversation between you and the compiler/interpreter. It has to be very precise in what it does. And since it defines what happens, it is the ultimate source of truth for how things will be handled. But it’s not the source of truth for everything that was in your head during the conversation.
That’s where comments come in. They’re a conversation with the next developer that provides context for the maintainer. Even (especially?) if it’s you. Things like why the code was written this way. The external constraints that had to be met. The choices not taken. Things that work fine now, but will be a problem later when scale changes.
And since they’re a conversation with another person they don’t have the limitations of whatever language you’re writing in. They can be about more than the why. You can talk about approximations. You can talk about generalities. You can talk about how this piece is expected to fit into the bigger picture without breaking your encapsulation. You can have simple artwork like flowcharts or truth tables. You can even have links to entire documents that provide even more context.
So don’t let anyone tell you that your code is fully self documenting and there’s no need to add comments. Your code isn’t, and there is a need.
Just don’t add comments like this
// Increment i i++;