Writing Good READMEs: Documentation That Developers Actually Read
The README is the front door of your project. Before anyone clones your repo, reads your code, or files an issue, they read your README. Most of them decide within 30 seconds whether your project is worth their time.
Most READMEs fail that test.
Why READMEs Matter (and Why Most Are Bad)
There’s a strange irony in software development: the same engineer who would never write a function without thinking carefully about its interface will write a README as an afterthought — a few bullet points dashed off after the “real work” is done.
But the README is an interface. It’s the interface between your project and everyone who might use it. A bad README has real costs:
- Evaluators move on to a different tool that explains itself better
- New users spend an hour getting the dev environment running instead of five minutes
- Your GitHub issues fill up with “how do I install this?” questions
- Contributors who could help give up because they can’t figure out how to run the tests
- Future you, returning to the project after six months, has no idea what past you was thinking
The most common README failure mode is “works on my machine” documentation — instructions that only make sense to the person who wrote them, written from the inside-out perspective of someone who already knows how everything fits together. The author knows what dependencies are required, what environment variables to set, and what the output should look like. They forget that nobody else does.
The other failure mode is treating the README as a checkbox. Something that has to exist. The minimum viable documentation to satisfy the formality of having a README. This README exists, technically. It just doesn’t help anyone.
Here’s the reframe: the README is marketing for your project. This is true even for internal projects. You’re trying to convince someone — a colleague, a future team member, your future self — that this project is worth using, worth contributing to, worth trusting. A good README does that. A bad README is a red flag.
The README Audience Problem
Before writing a single word, ask yourself: who is going to read this?
The answer is: multiple different people with completely different needs.
Evaluators arrive first. They’ve found your project through a search or a recommendation. They want to know: is this the right tool for my problem? They’ll spend 10–30 seconds on your README before deciding to dig deeper or move on. They need a clear description, a quick sense of what the project does, and ideally a screenshot or example that makes it concrete.
New users come next. They’ve decided to try your project. They need to go from zero to a working example. They need installation instructions that work, prerequisites that are explicit, and commands they can copy-paste without thinking. They’ll forgive a lot if they can get something running quickly.
Returning users are the reference seekers. They’ve used your project before and they’re coming back to look something up. They don’t need the introduction. They need to find the specific flag, config option, or API method they’re looking for — quickly. Good headings, a table of contents, and clear organization serve this audience.
Contributors need to understand how to set up the dev environment, run the test suite, and submit a pull request. They need the contributing section, not the sales pitch.
The problem with most READMEs is that they serve only one audience: the author. They’re written from the inside out — all the details the author knows are transcribed, but without the context a newcomer needs to make sense of them.
Start with the evaluator. They have the least patience and the most choice. If you can’t win them in the first two sentences, none of the rest matters.
The Essential Sections
A great README isn’t just a wall of text with a heading. It’s a structured document with distinct sections that serve different audiences. Here’s what belongs in a comprehensive README, roughly in order:
Project Name + One-Line Description
This sounds obvious, but an alarming number of READMEs open with a five-paragraph origin story when what the reader needs is: what is this?
The one-liner should describe what it does, not how it works. “A Python library for parsing CSV files” beats “An implementation of RFC 4180 using a streaming tokenizer.” Both may be accurate. Only one is useful to someone evaluating your project.
Badges
Badges are the vital signs of your project. A row of badges below the title tells evaluators at a glance:
- Does the build pass?
- What’s the test coverage?
- What version is current?
- What license does it use?
- How active is development?
A green build badge signals that someone is maintaining this. A broken badge (pointing to a CI system that no longer exists) signals the opposite. If your badges aren’t accurate, remove them.
|
|
A Screenshot or Demo
For anything visual — a web app, a CLI tool with interesting output, a dashboard — a screenshot or GIF is worth more than paragraphs of description. It answers the question “what does this actually look like?” instantly.
Keep images small. Huge screenshots that take 10 seconds to load on a slow connection are worse than no screenshot. Compress your PNGs. For animated demos, asciinema recordings embedded as SVGs are better than 20MB GIFs.
Why / Motivation
Why does this project exist? What problem does it solve that existing tools don’t? This section is often skipped, but it’s surprisingly important for evaluators — especially in crowded spaces where five similar tools exist.
If your project is a JSON parser, explain why you wrote another one. Faster? Streaming? Handles malformed input? Smaller dependency footprint? Give the reader a reason to pick yours.
Features
A concise bullet list of key capabilities. Keep it short — five to eight items. If you’re listing twelve features, you’re probably listing implementation details, not features.
Quick Start
The most important section for new users. More on this below.
Installation
Detailed setup instructions for all supported platforms. More on this too.
Usage
Real-world examples. The heart of the README.
Configuration
All options, flags, and environment variables — explained, with defaults shown.
API Reference
For libraries: either inline (small projects) or a link to generated documentation. For CLIs: a --help output or full flag reference.
Contributing
How to set up the dev environment, run the test suite, and submit a PR. Keep it short here; link to a CONTRIBUTING.md for more detail if needed.
License
One line, with a link. Evaluators need to know if they can actually use your project in their context.
Acknowledgments
Credits to upstream projects, inspirations, and contributors. Optional but good practice for open source.
Not every project needs every section. A small internal utility might not need an API reference or acknowledgments. A major open-source library might need a separate CONTRIBUTING.md, a docs site, and a changelog. Ruthlessly prioritize. A short, accurate README beats a long one where half the content is outdated.
The Opening Pitch: Nailing the First Paragraph
The first paragraph is your one chance to convert an evaluator. It needs to answer: what is this, who is it for, and why should I care?
There’s a formula that works:
[Project name] is a [type of tool] that [does X] for [audience] [without Y / unlike Z].
Some real examples that get this right:
- “csvkit is a suite of command-line tools for converting to and working with CSV, the king of tabular file formats.”
- “httpie is a command-line HTTP client. Its goal is to make CLI interaction with web services as human-friendly as possible.”
- “Caddy is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go.”
Notice what these have in common: concrete nouns, active verbs, specific audiences. Now look at what doesn’t work:
Bad:
“This is a project I made to help with file processing and related tasks.”
Bad:
“A highly scalable, enterprise-grade solution for modern data pipeline requirements.”
Good:
“csvtool converts Excel spreadsheets, JSON arrays, and TSV files to CSV in a single command. Handles files too large to open in Excel.”
Be specific. “Fast” means nothing. “Processes 1M rows/second on commodity hardware” means something. “Lightweight” means nothing. “Zero dependencies, 12KB binary” means something.
The tagline test: cover your README below the first two sentences. Can someone explain your project to a colleague based only on what’s visible? If not, rewrite until they can.
And wherever possible: show, don’t tell. A single working code example communicates more than three paragraphs of description.
Quick Start: Getting to “It Works” in 5 Minutes
This is the section that converts new users. The goal is to get them from zero to a working result in under five minutes. If it takes longer, you’ll lose most people.
The quick start should show the shortest possible path to a meaningful result — not a complete feature tour, not every configuration option, just: install, run, see something work.
Here’s what a good quick start looks like for a Python CLI tool:
|
|
Note what this does right:
- Comments explain each step
- Commands are copy-pasteable
- The expected output is shown — the reader knows what success looks like
For a Docker-based project, always include a one-liner:
|
|
Prerequisites should be explicit and versioned. Don’t assume the reader has the right Python version, has Homebrew installed, or is running Linux. Say:
Prerequisites: Python 3.10+, pip 23+
And include a verification step at the end — something the user can run to confirm the install worked:
|
|
The 5-minute rule is a hard constraint, not a guideline. If your quick start takes longer, the answer is not “add more steps” — it’s “simplify the project’s setup” or “offer a Docker option.”
Writing Good Installation Instructions
Installation is where most READMEs fall apart. The author knows what’s installed on their machine. The reader does not.
List all prerequisites explicitly. Don’t assume they have Homebrew, Node, Rust, or a specific version of Python. List everything the project needs to run before any installation commands. Include version requirements: “Node 20 LTS or later,” “Docker Engine 24.0+.”
Provide instructions for all supported platforms. If setup differs between Linux, macOS, and Windows, say so clearly. Use headers or tabs:
|
|
Don’t just say npm install or pip install — explain what it’s installing and why when it’s non-obvious. If the install pulls in a large native dependency, warn the reader: “This will compile a native module. Build time on a Raspberry Pi is about 3 minutes.”
The dependency hell problem: if your setup is complex — many dependencies, native compilation, specific system libraries — provide a Docker option as an escape hatch. A working Docker one-liner is worth more than a five-step native setup guide.
End with a verification command. Always. Give the reader something concrete to run that confirms everything is working. Nothing is more frustrating than following six steps of installation instructions and then not knowing if they worked.
Usage Examples: The Heart of the README
Usage examples are where most evaluators make their final decision and where most new users spend the most time. Get these right.
Use real-world examples, not foo, bar, baz, myapp, or example.com placeholders. Real-looking data makes examples instantly comprehensible. Compare:
Bad:
|
|
Good:
|
|
Start with the simplest possible use case, then progress to more complex ones. Your first example should be three to five lines. If the simplest working example is longer than that, your API may need simplification.
Show input AND output. What does running this command actually produce? Show it:
|
|
For libraries, show the import + a minimal working example:
|
|
For APIs, show the request and response together:
|
|
Annotate complex examples with comments. Don’t leave readers to guess what a flag does:
|
|
Every code block should be tested and actually work. Untested examples are worse than no examples — they erode trust the moment someone tries to run them.
Common README Anti-Patterns
Learn to recognize these. If your README does any of them, fix it.
The wall of text. No headings, no code blocks, no whitespace. A single gray block of prose. Unreadable. Nobody will push through this.
The changelog masquerading as a README. Version history at the top. Readers have to scroll past v0.1.0 through v2.3.4 to find how to install the thing. Put changelogs in CHANGELOG.md.
The assumptions problem. “Obviously you’ve already configured your AWS credentials.” “Assuming you have the standard dev toolchain installed.” Nothing is obvious. Assume the reader has a fresh machine and knows nothing about your project’s ecosystem.
Stale documentation. Instructions that no longer work because the project changed but the README didn’t. This is trust-destroying. A reader who follows your README, hits an error, and discovers the instructions are outdated will not come back.
The academic README. “This system implements a novel approach to tabular data transformation leveraging a streaming tokenizer to achieve O(n) complexity.” Who cares? Show how to use it.
No examples. Describing what the tool does without showing it. “csvtool supports filtering, transformation, format conversion, and statistical analysis.” Great. What does that look like?
Broken badges. Badges that point to CI that doesn’t exist anymore, coverage reports that are months old, version badges stuck at 0.1.0. Audit your badges. Remove broken ones.
The “just read the code” README. “For full documentation, see the source.” The source is not documentation. Linking to source as documentation is an abdication.
Platform assumptions. brew install myapp with no Linux or Windows alternative. apt-get install dependency with no macOS equivalent. Be explicit about what platforms you support and what to do on each.
Markdown Craft: Making Your README Scannable
Developers don’t read READMEs — they scan them. Your job is to make scanning productive.
Use headings to create structure. GitHub auto-generates a table of contents from your headings. Use ## for major sections and ### for subsections. Don’t go deeper than #### — it signals that the section needs to be broken up or moved to a separate document.
Code blocks for everything. Commands, output, configuration files, environment variables, JSON payloads — all of it goes in code blocks. Inline code (backticks) for short references; fenced blocks (triple backticks) for multi-line content. Always specify the language for syntax highlighting:
|
|
Use bold sparingly, for genuine emphasis. If everything is bold, nothing is.
Bullet lists for features and options; numbered lists for sequential steps. A numbered list implies order matters. If the reader can do the steps in any order, use bullets.
Tables for option references and comparison matrices:
|
|
Collapsible sections for long content. Troubleshooting guides, full configuration references, and extended changelogs can be collapsed with <details>:
|
|
GitHub-flavored Markdown alerts for warnings and notes:
|
|
Preview before pushing. Use grip (a local Markdown renderer that mirrors GitHub’s rendering exactly) to see what your README will look like:
|
|
Tools and Workflows
A few tools that make writing and maintaining READMEs easier:
Shields.io — the standard for badge generation. Covers build status, coverage, version, license, download counts, and much more. Generates the Markdown for you.
grip — renders your Markdown locally using GitHub’s API, so what you see is exactly what GitHub will show. Essential for catching rendering issues before they go live.
doctoc — automatically generates and updates a table of contents from your headings. Run it as a pre-commit hook so the TOC never goes stale:
|
|
Vale — a prose linter for technical writing. Catches passive voice, unclear language, wordy phrasing, and style guide violations. Works in your editor and CI:
|
|
asciinema — records terminal sessions as lightweight, text-based animations. Embed recordings on your README with a link to asciinema.org, or export as SVG. Far better than GIFs for terminal demos.
readme-md-generator — generates a README scaffold by reading your package.json, .git config, and other project metadata. Good for getting started.
Keeping READMEs current: the most important workflow change you can make. Add “Update README if needed” to your PR template. If your code changes how something is installed, configured, or used, the README must change with it. Treat a stale README as a bug.
README Templates
Template: Open Source Library or CLI
Copy and adapt this for a library, CLI tool, or open-source project:
|
|
Template: Internal / Homelab Project
For an internal tool, self-hosted service, or homelab project — where the audience is your team or your future self:
|
|
The Living README: Keeping It Current
A README written once and never updated is a liability. It starts accurate and becomes progressively less trustworthy as the code evolves without it.
Treat the README as a contract. If your code changes how something is installed, configured, or used, the README must change with it. A breaking change without a README update is an incomplete change.
Add README updates to your PR template:
|
|
The “fresh eyes” test is the most reliable check: find someone who hasn’t seen the project before and ask them to follow the README from scratch. Watch where they get confused, where they have to ask questions, where they hit errors. Fix every one of those points. If you can’t find a willing victim, wait six months — your future self qualifies.
For API-level documentation, generate from code, don’t write by hand. Tools like pdoc (Python), typedoc (TypeScript), godoc (Go), and rustdoc (Rust) extract documentation from docstrings and type annotations. Link to the generated docs from your README rather than maintaining a separate reference by hand. The generated docs will stay accurate as long as the code is documented — the README just needs to link to them.
README in git means history is preserved. You can always git log -- README.md to see how the docs evolved. You can diff any two versions. When a release breaks something, you can find exactly when the documentation changed. This is free, and it’s another reason to keep everything in the repository.
A great README is one of the highest-leverage investments you can make in a project. It costs a few hours to write well. It pays back every time someone successfully evaluates, installs, and uses your project without needing to ask you a question.
The bar is low. Most READMEs are bad. Be the project that people recommend because the documentation is actually good.
Comments