Generate CHANGELOG from Conventional Commits in Python

Parse your git log for conventional commits (feat, fix) and produce a simple Markdown CHANGELOG with grouped features and bug fixes.

Medium Python 3.9+ Aug 9, 2026 Git + Python 14 views 0 copies

Python code

57 lines
Python 3.9+
import subprocess
import re
import sys
from collections import OrderedDict

CONVENTIONAL_COMMIT = re.compile(
    r"^(?P<type>feat|fix|chore|docs|refactor|perf|test|build|ci|style)(?:\((?P<scope>[^)]+)\))?: (?P<description>.+)"
)


def get_git_log():
    return subprocess.run(
        ["git", "log", "--format=%s"],
        capture_output=True,
        text=True,
        check=True,
    ).stdout.splitlines()


def parse_conventional_commits(commits):
    grouped = OrderedDict()
    for commit in commits:
        match = CONVENTIONAL_COMMIT.match(commit)
        if match:
            commit_type = match.group("type")
            scope = match.group("scope")
            description = match.group("description")
            if commit_type in ("feat", "fix"):
                prefix = f"**{scope}:** " if scope else ""
                grouped.setdefault(commit_type, []).append(f"{prefix}{description}")
    return grouped


def generate_changelog():
    commits = get_git_log()
    parsed = parse_conventional_commits(commits)

    changelog_lines = ["# Changelog", ""]
    if "feat" in parsed:
        changelog_lines.append("## Features")
        for item in parsed["feat"]:
            changelog_lines.append(f"- {item}")
        changelog_lines.append("")
    if "fix" in parsed:
        changelog_lines.append("## Bug Fixes")
        for item in parsed["fix"]:
            changelog_lines.append(f"- {item}")
        changelog_lines.append("")
    return "\n".join(changelog_lines).strip()


if __name__ == "__main__":
    try:
        print(generate_changelog())
    except subprocess.CalledProcessError:
        print("Error: not a git repository", file=sys.stderr)
        sys.exit(1)

Output

stdout
# Changelog

## Features
- **auth:** add login endpoint
- **ui:** improve button styling

## Bug Fixes
- **db:** fix connection timeout
- apply retry logic

How it works

The script runs git log --format=%s to get each commit's subject line, then the regex extracts the commit type, optional scope, and description. Only feat and fix types are kept, since those are the main user-facing changes. An OrderedDict preserves the order commits appear in the log, so the changelog reflects the actual timeline. The result is a Markdown string with a Features section and a Bug Fixes section, which you can redirect to a CHANGELOG.md file.

Common mistakes

  • Forgetting to run the script inside a git repository — it exits with an error.
  • Not using `--format=%s`, which would include the full commit body and break parsing.
  • Assuming all commit types are included, but `chore`, `docs`, etc. are intentionally skipped.
  • Not handling merge commits or other non-conventional subjects — they are safely ignored.

Variations

  1. Add version grouping by parsing `git tag` to split the changelog per release.
  2. Use a library like `python-semantic-release` to generate and bump versions automatically.

Real-world use cases

  • Automating a release process that updates CHANGELOG.md in CI before publishing a package.
  • Generating release notes for an internal tool from a shared monorepo's commit history.
  • Creating user-friendly summaries of changes for clients from a project's git log.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Git + Python

Related tutorials and quizzes for this topic.