Team work and collaboration

Write a changelog entry that tells readers what changed and what to do

Turn raw release notes into a changelog entry that separates changes from required user actions and verification steps. A synthetic software update shows how to make a release note useful to someone who did not make the change.

Show contents

Who this is forThis guide is for teams that need release notes or project changelogs that tell readers both what changed and what they must do next.

What you need
  • Python 3.12 and a terminal command that starts that version.
  • A text editor that can save UTF-8 JSON and text files.
  • A working folder where the script can create a new folder beneath outputs.
  • Only the Python standard library is required: json and pathlib.

01Separate the change from the reader's action

A changelog entry should answer at least three practical questions: what changed, what the reader needs to do, and how the reader can check that the update worked. A list of implementation details may be accurate but still leave another team member unsure whether any action is required.

This tutorial uses a small synthetic release with three changes. Each change has an area, a factual description, and a required action. A separate verification list tells the reader what to check after updating.

02Create synthetic raw release notes

The following release information is synthetic and was written specifically for this article. Save it as release_notes.json. It describes version 1.4.0 of a fictional CSV export tool.

json
{
  "version": "1.4.0",
  "date": "2026-09-20",
  "changes": [
    {
      "area": "Export path",
      "change": "Default CSV export folder changed from reports/ to outputs/reports/.",
      "action": "Update scripts or shortcuts that expect reports/."
    },
    {
      "area": "Config key",
      "change": "Configuration key report_dir was renamed to output_dir.",
      "action": "Rename report_dir to output_dir before the next run."
    },
    {
      "area": "Validation",
      "change": "Empty customer_id values now stop export instead of being written as blank cells.",
      "action": "Fix blank customer_id values before rerunning failed exports."
    }
  ],
  "checks": [
    "Confirm a test export appears under outputs/reports/.",
    "Confirm the configuration uses output_dir.",
    "Confirm a row with a blank customer_id stops with a validation error."
  ]
}

There are exactly 3 changes, 3 required actions, and 3 verification checks. Because the example is synthetic, these paths, configuration names, and behaviors are demonstration values rather than facts about a real product.

03Work out the expected changelog entry by hand

The entry should begin with the version and date. The change descriptions should stay factual. Required actions should use direct instructions, and verification should be separate so readers can distinguish configuration work from post-update checks.

text
Version 1.4.0 - 2026-09-20

What changed
- Export path: Default CSV export folder changed from reports/ to outputs/reports/.
- Config key: Configuration key report_dir was renamed to output_dir.
- Validation: Empty customer_id values now stop export instead of being written as blank cells.

What you need to do
- Update scripts or shortcuts that expect reports/.
- Rename report_dir to output_dir before the next run.
- Fix blank customer_id values before rerunning failed exports.

Check after updating
- Confirm a test export appears under outputs/reports/.
- Confirm the configuration uses output_dir.
- Confirm a row with a blank customer_id stops with a validation error.

The expected entry contains one heading line, three section headings, and nine bullet lines: three changes, three actions, and three checks. No action is hidden inside a paragraph describing the change.

04Generate and validate the changelog entry

Save the following script as useful_changelog.py. It validates the synthetic JSON, creates the changelog text, checks the expected section and bullet counts, and writes the result into a new output folder. The original JSON file is read only.

python
import json
from pathlib import Path

SOURCE = Path("release_notes.json")
OUTPUT_DIR = Path("outputs") / "useful_changelog_result"
OUTPUT = OUTPUT_DIR / "CHANGELOG_ENTRY.txt"


def require_text(value, name):
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{name} must be a non-empty string.")
    return value.strip()


def main() -> None:
    if not SOURCE.is_file():
        raise FileNotFoundError(f"Source file not found: {SOURCE}")
    if OUTPUT_DIR.exists():
        raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")

    with SOURCE.open("r", encoding="utf-8") as stream:
        data = json.load(stream)

    version = require_text(data.get("version"), "version")
    date = require_text(data.get("date"), "date")
    changes = data.get("changes")
    checks = data.get("checks")

    if not isinstance(changes, list) or not changes:
        raise ValueError("changes must be a non-empty list.")
    if not isinstance(checks, list) or not checks:
        raise ValueError("checks must be a non-empty list.")

    change_lines = []
    action_lines = []
    for index, item in enumerate(changes, start=1):
        if not isinstance(item, dict):
            raise ValueError(f"Change {index} must be an object.")
        area = require_text(item.get("area"), f"change {index} area")
        change = require_text(item.get("change"), f"change {index} description")
        action = require_text(item.get("action"), f"change {index} action")
        change_lines.append(f"- {area}: {change}")
        action_lines.append(f"- {action}")

    check_lines = [f"- {require_text(value, 'check')}" for value in checks]

    lines = [
        f"Version {version} - {date}",
        "",
        "What changed",
        *change_lines,
        "",
        "What you need to do",
        *action_lines,
        "",
        "Check after updating",
        *check_lines,
    ]
    text = "\n".join(lines) + "\n"

    if text.count("\nWhat changed\n") != 1:
        raise RuntimeError("Missing What changed section.")
    if text.count("\nWhat you need to do\n") != 1:
        raise RuntimeError("Missing action section.")
    if text.count("\nCheck after updating\n") != 1:
        raise RuntimeError("Missing verification section.")

    bullet_count = sum(line.startswith("- ") for line in lines)
    expected_bullets = len(changes) * 2 + len(checks)
    if bullet_count != expected_bullets:
        raise RuntimeError("Unexpected bullet count.")

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()
    with OUTPUT.open("x", encoding="utf-8") as stream:
        stream.write(text)

    print(f"Changes: {len(changes)}.")
    print(f"Required actions: {len(action_lines)}.")
    print(f"Verification checks: {len(checks)}.")
    print(f"Total bullets: {bullet_count}.")
    print(f"Output: {OUTPUT.as_posix()}")


if __name__ == "__main__":
    main()

05Check the expected result

For this synthetic release, the script should report three changes, three required actions, three verification checks, and nine bullet lines. The expected console output below was derived by hand and is not an execution log.

text
Changes: 3.
Required actions: 3.
Verification checks: 3.
Total bullets: 9.
Output: outputs/useful_changelog_result/CHANGELOG_ENTRY.txt
  • Confirm that each change appears once under What changed.
  • Confirm that every change has a corresponding instruction under What you need to do.
  • Confirm that verification steps are separated from required actions.
  • Confirm that paths and configuration names are copied exactly from the synthetic source notes.
  • Run the script again without changing OUTPUT_DIR. It should stop with FileExistsError instead of replacing the previous entry.

06Recognize changelog entries that are technically correct but not useful

Weak changelog textWhat is missing
Updated export logicReaders do not know which behavior changed or whether they need to act.
Fixed configurationThe affected key and required replacement are not identified.
Improved validationThe new failure condition and effect on users are unclear.
Various bug fixesThere is no information about impact, scope, or required checks.
Please update accordinglyThe action is too vague to execute or verify.

Avoid making the reader reconstruct the change from issue trackers, commits, or chat history. If an update changes a path, key, command, file format, or required input, name the old and new behavior directly.

07Keep the changelog scoped to user-visible consequences

A changelog entry does not replace technical documentation, migration instructions, test evidence, or version control history. Complex changes may need links to those materials, but the changelog should still summarize the consequence and immediate action.

Not every internal refactor needs a changelog entry. If behavior, interfaces, dependencies, required inputs, outputs, configuration, or workflow do not change for the reader, detailed implementation notes may belong elsewhere.

For larger releases, group related changes by area and distinguish required actions from optional recommendations. Preserve older changelog entries instead of rewriting history unless you are correcting a documented error and can record that correction clearly.

Execution and verification record

2026-09-20 · hand-checked example · target: Python 3.12 · standard library: json, pathlib · no execution

  • Manually counted 3 synthetic changes, 3 corresponding required actions, and 3 verification checks.
  • Manually derived the expected changelog entry with three named sections.
  • Calculated the expected total as 9 bullet lines: 3 changes + 3 actions + 3 checks.
  • Checked that the synthetic old and new export paths are reports/ and outputs/reports/ and that the configuration names are report_dir and output_dir.
  • Inspected the script for required text validation, section checks, bullet-count validation, output collision protection, and preservation of the source JSON.
  • Derived the expected console output by hand.
Verification limits
  • The code was not executed by the author of this response; no changelog file was created.
  • The version, paths, configuration keys, and validation behavior are synthetic examples and do not describe a real product.
  • The script validates structure but cannot determine whether a human-written change description is complete or accurate.
  • The official documentation URLs were provided from known documentation locations but were not checked live.

Site-wide writing and verification principles

References

The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.