Python Changelog Automation — Core Concepts
Why Changelogs Matter
A changelog is a trust signal. When a library publishes clear, categorized release notes, users upgrade confidently. When the changelog says “misc fixes” or is empty, users assume the worst and pin old versions forever.
Automated changelogs solve two problems: the human tendency to forget, and the inconsistency of different people writing in different styles.
Two Approaches
Commit-Based (python-semantic-release)
Every commit message follows a convention. At release time, a tool reads the commit log and generates the changelog.
Conventional Commits format:
feat: add retry logic to HTTP client
fix: handle null response in parser
docs: update installation guide
feat!: change default timeout from 30s to 10s
BREAKING CHANGE: Default timeout reduced. Pass timeout=30 to restore old behavior.
The tool groups these into sections:
## v1.5.0 (2026-03-28)
### Features
- Add retry logic to HTTP client (#142)
### Bug Fixes
- Handle null response in parser (#139)
### Breaking Changes
- Change default timeout from 30s to 10s (#145)
Fragment-Based (towncrier)
Each change gets a small file (a “fragment”) in a changes/ directory. At release time, towncrier assembles them into a changelog entry and deletes the fragments.
changes/
├── 142.feature.md → "Added retry logic to HTTP client"
├── 139.bugfix.md → "Fixed null response handling in parser"
└── 145.breaking.md → "Default timeout changed from 30s to 10s"
Why fragments? They eliminate merge conflicts. Two developers adding changelog entries in different PRs never touch the same file — each creates a uniquely named fragment.
Setting Up Conventional Commits
Step 1: Enforce the Format
Use pre-commit to reject non-conforming messages:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.1.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
Step 2: Configure the Generator
# pyproject.toml
[tool.semantic_release]
version_toml = ["pyproject.toml:project.version"]
changelog_file = "CHANGELOG.md"
commit_parser = "angular"
Step 3: Generate on Release
semantic-release changelog
# or as part of a full release:
semantic-release version --changelog
Setting Up towncrier
Step 1: Configure
# pyproject.toml
[tool.towncrier]
directory = "changes"
filename = "CHANGELOG.md"
package = "my_package"
[[tool.towncrier.type]]
directory = "feature"
name = "Features"
showcontent = true
[[tool.towncrier.type]]
directory = "bugfix"
name = "Bug Fixes"
showcontent = true
[[tool.towncrier.type]]
directory = "breaking"
name = "Breaking Changes"
showcontent = true
Step 2: Add Fragments in PRs
echo "Added retry logic to HTTP client" > changes/142.feature.md
git add changes/142.feature.md
Step 3: Build the Changelog
towncrier build --version 1.5.0
towncrier appends the new section to CHANGELOG.md and deletes all fragment files.
Which Approach to Choose?
| Factor | Commit-Based | Fragment-Based |
|---|---|---|
| Merge conflicts | Possible in CHANGELOG.md | None (separate files) |
| Developer effort | Write good commits (should do anyway) | Create a fragment file per PR |
| Control over wording | Limited to commit messages | Full editorial control |
| Best for | Libraries with automated CI releases | Projects needing polished release notes |
| CI integration | Fully automatic | Semi-automatic (fragments are manual) |
Many teams combine both: conventional commits for automated version bumps, plus towncrier fragments for user-facing release notes that need a human touch.
Common Misconception
“Automated changelogs are just git logs.” A raw git log --oneline is not a changelog. It includes internal refactors, typo fixes, and merge commits that users do not care about. Automated tools filter, group, and format — turning developer noise into user signal.
CI Integration Pattern
A complete release pipeline:
- PR merges to
mainwith conventional commit messages - CI runs
semantic-release version— bumps version, updatesCHANGELOG.md, creates git tag - CI runs
semantic-release publish— pushes tag, creates GitHub Release with changelog as body - Separate workflow triggers on tag push — builds wheel, publishes to PyPI
This flow means no human touches the version number or the changelog after the initial commit message.
The one thing to remember: Pick commit-based automation for speed or fragment-based for polish — either way, your changelog stays accurate without anyone remembering to update it.
See Also
- Python Black Formatter Understand Black Formatter through a practical analogy so your Python decisions become faster and clearer.
- Python Bumpversion Release Change your software's version number in every file at once with a single command — no more find-and-replace mistakes.
- Python Ci Cd Python Understand CI CD Python through a practical analogy so your Python decisions become faster and clearer.
- Python Cicd Pipelines Use Python CI/CD pipelines to remove setup chaos so Python projects stay predictable for every teammate.
- Python Commitizen Conventional Commits Write git commit messages that follow a pattern so tools can automatically version your software and write your changelog.