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.
Python code
57 linesimport 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
# 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
- Add version grouping by parsing `git tag` to split the changelog per release.
- 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
More from Git + Python
- Amend Last Commit Message in Python easy
- Bisect Good Bad Automation Script in Python easy
- Build a Simple Log Graph in Python easy
- Bump Semantic Version Git Tag in Python easy
- Count Unique Contributors from Git Shortlog in Python easy
- Create a Mock GitHub Release API in Python for Testing gh CLI easy
Keep learning
Related tutorials and quizzes for this topic.