How to Write Good Code Documentation (A Practical Guide)
Most code documentation fails for one of two reasons: there's too little of it, or there's too much of the wrong kind. Good documentation answers the questions a reader actually has, in the order they have them, and sheer volume has very little to do with it. This guide breaks down what that looks like in practice.
Start with why, not what
A reader skimming a function can usually see what it does. What they can't see is why it exists, when to use it, and what will break if they change it. Lead with intent:
- What problem does this module solve?
- Who calls it, and in what situation?
- What are the assumptions and side effects?
If your documentation only restates the code in English, it adds maintenance cost without adding understanding.
The anatomy of a good function doc
For any non-trivial function, aim to cover:
- A one-line summary of what it does, in plain language.
- Every parameter: its name, its type, and what it actually means, not just what type it is.
- The return value, again with both the type and the meaning.
- Side effects. Does it write to disk, hit the network, mutate inputs?
- A usage example, which is the single most valuable thing you can include.
A real example is worth more than three paragraphs of prose. It shows the reader exactly how the pieces fit together.
Write for the next maintainer, not the compiler
The audience for documentation is a human who will read your code months from now, quite possibly you. Optimize for that person:
- Use the vocabulary of the domain, not just the codebase.
- Call out the non-obvious. If something looks like a bug but isn't, say so.
- Skip the obvious.
i++ // increment iis noise.
Before and after
Here is a function documented the way most code is, which is to say restating the code:
def retry(fn, attempts=3, delay=1.0):
"""Retries fn."""
And the same function documented for the person who has to call it:
def retry(fn, attempts=3, delay=1.0):
"""Call fn until it succeeds, waiting between failed attempts.
Args:
fn: A zero-argument callable. Exceptions it raises count as failures.
attempts: Total tries, including the first. Must be at least 1.
delay: Seconds to wait after a failure, doubled after each retry.
Returns:
Whatever fn returns on its first successful call.
Raises:
The last exception fn raised, if every attempt failed.
Example:
data = retry(lambda: client.get("/status"), attempts=5)
"""
The second version answers every question a caller has: what counts as failure, whether attempts includes the first try, that the delay grows, and what happens when everything fails. None of that is visible from the signature, which is exactly why it belongs in the documentation.
Four levels, four jobs
Documentation is not one thing. A healthy codebase has four levels, and each answers a different question.
- Inline comments answer why this line is the way it is. Use them sparingly, for the non-obvious: a workaround for a library bug, a performance trick, a business rule that looks wrong but isn't.
- Function and class docs answer how do I use this. This is the level covered above, and the one readers hit most often, usually on hover in their editor.
- Module docs answer what lives here and when do I reach for it. A short paragraph at the top of a file saves a new reader from opening five functions to work it out.
- READMEs and architecture notes answer how do the pieces fit together. This is where a new teammate starts, and the level most often missing.
Teams that only write level 2 end up with well-documented functions nobody can find. Teams that only write level 4 end up with a beautiful overview that stops matching the code within a month. You need a little of each.
Your docs have a new reader
The person reading your documentation is no longer always a person. AI coding assistants read your repository too, and they treat what your docs say as fact. A stale description of a function gets confidently repeated into new code, review comments and answers.
That raises the cost of wrong documentation and lowers the cost of missing documentation, because an assistant can read the code itself but cannot tell that a paragraph about it is out of date. Accurate, current docs now help every reader of your code, human or not.
Keep docs close to the code
Documentation that lives far from the code it describes drifts almost immediately. Keep it in the repo, next to what it documents, and update it in the same commit as the code change. Docs in a separate wiki that nobody updates are worse than no docs, because they actively mislead.
Documentation goes stale
Even well-written docs rot. The function gets refactored, the parameter gets renamed, the example stops compiling, and the docs quietly become wrong. This is the single biggest reason teams give up on documentation entirely.
The fix has two halves. First, treat documentation as something you regenerate, not something you hand-maintain forever: when generating docs takes one click and a few seconds, keeping them current stops being a chore you skip. Second, make staleness visible. If you can see that a doc is three commits behind its source file, you can fix it; if you can't, you find out when someone gets burned. We wrote about measuring that in What Is Documentation Drift, and How Do You Catch It?.
That's exactly the gap Writulos Scribe was built to close. Paste any code, or wire it into your CI pipeline, and get clean, structured documentation with function breakdowns, parameters, return types, and plain-English descriptions, every time. It documents only what's actually in your code, so you never get invented behavior. And because regenerating is instant, your docs can stay in step with your code instead of falling behind it.
A simple checklist
Before you call documentation "done," check that a new reader could answer:
- What is this for?
- How do I use it? (with an example)
- What are the inputs and outputs?
- What should I be careful about?
- Is this still true? (when was it last checked against the code?)
If all five are covered, you've written documentation that earns its place in the repo.
Common mistakes to avoid
- Documenting the implementation instead of the contract. Callers need to know what a function promises, not how it keeps the promise. Implementation details change; contracts shouldn't.
- Copying the type into the prose. If the signature says
user_id: int, "an integer" adds nothing. Say what the integer means. - Examples that don't run. A broken example is worse than none. Where your language allows it, run examples as tests.
- Writing docs once. Documentation is part of the change, not a project that ends. If a pull request changes behavior and not the docs, it isn't finished.