How to keep a changelog for a Python package on PyPI
PyPI and pip give you more release tooling than most maintainers use: a well-known Changelog link, pre-releases pip skips by default, yanking and Requires-Python. Here's how to tie them to a changelog people can upgrade from.
Updated Last checked 5 min readOpen-source libraries and packages
The short answer
- Add a
ChangelogURL under[project.urls]in pyproject.toml; PyPI lists it with the project's links. - Use PEP 440 versions. pip skips pre-releases like
2.0.0rc1unless asked with--preor a specifier that names one. - Yank broken releases instead of deleting them, and mark them
[YANKED]in the changelog. - Raise
requires-pythonin the release that drops a Python version, and mark that entry**Breaking:**in the changelog. - Warn in code with
DeprecationWarning(or@warnings.deprecatedon Python 3.13+) before you remove anything.
Changelog entry for a Python package
A major release that drops a Python version, plus a yanked patch release, in Keep a Changelog 2.0.0 format.
## [2.0.0] - YYYY-MM-DD
### Changed
- **Breaking:** Python 3.10 or later is required (`requires-python = ">=3.10"`).
On Python 3.9, pip keeps installing 1.x.
- **Breaking:** `Client.fetch()` is now async. Replace `client.fetch(url)` with
`await client.fetch(url)` inside an async function.
### Deprecated
- `Client.fetch_sync()`: use `asyncio.run(client.fetch(url))`.
It emits a DeprecationWarning. It will be removed in 3.0.0.
### Removed
- **Breaking:** `mylib.compat`, deprecated since 1.6.0.
### Fixed
- Timeouts now apply to DNS lookups as well as connections (#211).
## [1.9.1] - YYYY-MM-DD [YANKED]
Yanked: the wheels were built without the C extension. Use 1.9.2.
[2.0.0]: https://github.com/OWNER/REPO/compare/v1.9.2...v2.0.0mylib, its API and the versions are placeholders.
Free tool · no sign-up
Can a coding agent read your changelog?
Paste your repository, a GitHub release or a CHANGELOG.md link. The free converter shows every version, breaking change, migration step and deprecation it can extract, as JSON plus an llms.txt summary.
No AI and nothing stored. Or paste a CHANGELOG.md instead
01Link the changelog from your package metadata
Add your changelog to [project.urls]. PyPI shows these URLs as project links on the package page, and the well-known project URLs specification recognises a Changelog label (with aliases such as ‘What's New’ and ‘History’) and a separate Release Notes label, so tools know which link is which.
[project]
name = "mylib"
version = "2.0.0"
requires-python = ">=3.10"
[project.urls]
Homepage = "https://example.com"
Documentation = "https://example.com/docs"
Repository = "https://github.com/OWNER/REPO"
Changelog = "https://github.com/OWNER/REPO/blob/main/CHANGELOG.md"02Version with PEP 440, and know what pip installs
Python versions follow PEP 440: pre-releases look like 2.0.0a1, 2.0.0b2 or 2.0.0rc1, post-releases like 2.0.0.post1 and development releases like 2.0.0.dev3. pip ignores pre-releases and development releases unless you pass --pre or a specifier that includes one (such as >=2.0.0rc1), so a release candidate won't reach people by accident.
Give each pre-release its own changelog entry, then let the final release's entry summarise everything since the last stable version.
03Yank, don't delete
If a release is broken, yank it on PyPI. Under PEP 592, installers ignore a yanked release unless it's pinned exactly with == or ===, so pinned installs keep working while new installs skip it. Deleting a release can't be undone, and PyPI won't let you upload a file with the same name again.
Keep the entry in your changelog and mark it with [YANKED] after the date, plus one line saying why and which version to use instead. Keep a Changelog 2.0.0 is blunt about it: “List it; do not hide it.”
04Dropping a Python version
Raise requires-python in the release that drops support. pip compares it with the running interpreter and picks the newest release that still supports it, so people on the old Python quietly stay on your previous version (see the Python Packaging guide). Mark the entry **Breaking:** in the changelog anyway: people who pin versions or build from source still need to know.
05Warn before you remove
Emit a DeprecationWarning from anything you plan to remove, and name the replacement in the message. Python hides DeprecationWarning by default except in code run directly as __main__ (PEP 565), but pytest shows it by default, which is where many of your users will see it.
import warnings
def fetch_sync(url):
warnings.warn(
"fetch_sync() is deprecated and will be removed in 3.0.0; "
"use asyncio.run(client.fetch(url)) instead.",
DeprecationWarning,
stacklevel=2,
)
...
# Python 3.13+: the decorator also tells type checkers (PEP 702).
# On older versions: from typing_extensions import deprecated
from warnings import deprecated
@deprecated("Use asyncio.run(client.fetch(url)); removal planned for 3.0.0.")
def fetch_sync(url): ...Then list it under Deprecated in the changelog with the replacement and the planned removal version. More in how to write deprecation notices.
06Collect entries with news fragments
On a project with many contributors, merge conflicts in CHANGELOG.md get old fast. towncrier has each pull request add a small ‘news fragment’ file; at release time it assembles them into the changelog and removes the fragments. pip uses it: fragments go in a news/ directory and are compiled into NEWS.rst.
# pyproject.toml
[tool.towncrier]
directory = "changes"
filename = "CHANGELOG.md"
# In a pull request: add one fragment per change
towncrier create 123.feature.md --content "parse() accepts pathlib.Path objects."
# At release time: build the changelog section for the new version
towncrier build --version 2.1.0Before and after
Too vague
2.0: Python 3 cleanup.
Specific and actionable
2.0.0 requires Python 3.10+ (pip keeps Python 3.9 users on 1.x). Client.fetch() is now async: replace client.fetch(url) with await client.fetch(url). fetch_sync() is deprecated, warns when called, and is planned for removal in 3.0.0.
Fictional example: use your own verified names, versions and behavior.
LogPulse
Generate release notes with LogPulse
LogPulse is a hosted changelog that people and coding agents can both read. Here's exactly what it can do for the steps above, and what it can't.
Check your changelog, free
Paste a CHANGELOG.md or a GitHub releases link. A deterministic parser (no AI, nothing stored) shows the versions, breaking changes, migration steps and deprecations an agent can extract.
Draft notes from GitHub
Connect a repository: merged pull requests, commits to your default branch and published GitHub releases collect in an inbox. AI drafts a note in the tone you pick, listing breaking changes and deprecations only when the changes clearly show them. Nothing is published until you approve it. 3 AI drafts on Free; unlimited on Open source and Pro. Pro opens soon: .
Publish once, read everywhere
Approved notes go to a hosted changelog and an in-app widget, to email subscribers on Open source and Pro (Pro opens soon), and to a JSON feed and MCP server coding agents can query. CI and agents can publish with an API key.
What it doesn't do: LogPulse doesn't tag releases, bump versions, publish to npm or PyPI, or write CHANGELOG.md back to your repository; keep your release tooling for those. AI drafts can miss context, so review them like a pull request.
Before you publish
- A Changelog URL is set under [project.urls]
- The version is valid PEP 440 and matches the git tag
- Pre-releases have their own entries
- Yanked releases are marked [YANKED] with a reason
- requires-python is raised when a Python version is dropped
- Deprecated APIs emit a DeprecationWarning that names the replacement
Common questions
Should the changelog be Markdown or reStructuredText?
Either; GitHub renders both. Projects with Sphinx documentation often use reStructuredText so the changelog becomes part of the docs (pip's NEWS.rst is one example). Pick one and keep the headings consistent.
Does PyPI show my changelog?
PyPI shows your project description, usually the README, and the project links from your metadata. Link the changelog with a Changelog URL so people can find it from the package page.
What's the difference between yanking and deleting a release?
Yanking keeps the files available to exact pins but hides the release from normal resolution. Deleting removes it permanently, can break pinned installs, and the file names can't be reused.
How do coding agents find my Python changelog?
Usually through the repository. Clear version headings and Breaking and Deprecated sections make it parseable. Check what an agent would extract with the changelog-to-JSON converter, which also reads reStructuredText-style headings.
Sources
Facts last checked against these sources on .
Read next
How to keep a changelog for an npm package
Where npm users look for changes, how to keep versions, git tags and headings in sync, ship prereleases without moving latest, and deprecate old versions.
Read the guide Breaking changes, deprecations and versioningHow to write deprecation notices
Say what is deprecated, what replaces it and when it will be removed, without inventing deadlines. A before/after deprecation notice and a 5-point checklist.
Read the guide Open-source libraries and packagesHow to write release notes for open-source libraries
How to write release notes: a copy-paste template, 3 examples (library, CLI, breaking change), a 7-point checklist and how Django and Node.js do it.
Read the guide Breaking changes, deprecations and versioningHow to announce breaking changes
When and where to warn users about a breaking change: deprecate in a minor first, ship the major with migration steps, reach every channel. Template included.
Read the guide Open-source libraries and packagesCHANGELOG.md format, explained line by line
The CHANGELOG.md format most projects follow: version headings, YYYY-MM-DD dates, the six change types, Breaking markers, Unreleased, yanked versions and links.
Read the guideMore guides: open-source libraries and packages
- Changelog vs release notes: what's the difference?
- Release notes templates in Markdown
- Keep a Changelog vs GitHub Releases
- How to generate a changelog from git commits
- How to write release notes from commits (Conventional Commits)
- semantic-release vs release-please vs Changesets (and git-cliff)
Tools and resources
LogPulse
Changelogs coding agents can upgrade from.
Publish versions, breaking changes, migration steps and deprecations once. People get a widget and hosted changelog; agents get a JSON feed and MCP server. Free for open source.