Python Bumpversion Release — Deep Dive

The Parse/Serialize System

bump-my-version’s power comes from its regex-based version parsing and multi-format serialization. Understanding this system unlocks custom versioning schemes.

How It Works

  1. Parse: A regex extracts named groups from the current version string
  2. Bump: The tool increments the specified group and resets downstream groups
  3. Serialize: The new version is formatted using one of the serialize templates
[tool.bumpversion]
current_version = "2.1.0-beta.3"
parse = '''(?x)
    (?P<major>\d+)\.
    (?P<minor>\d+)\.
    (?P<patch>\d+)
    (-(?P<stage>alpha|beta|rc)\.(?P<stage_n>\d+))?
'''
serialize = [
    "{major}.{minor}.{patch}-{stage}.{stage_n}",
    "{major}.{minor}.{patch}"
]

The (?x) flag enables verbose regex mode. Multiple serialize formats are tried in order — the first one that produces a complete string (no empty groups) wins.

Reset Rules

When you bump minor, all groups to its right reset:

  • 2.1.0-beta.32.2.0 (patch resets to 0, pre-release disappears)

When you bump stage_n:

  • 2.1.0-beta.32.1.0-beta.4 (only the stage number increments)

When you bump stage:

  • 2.1.0-alpha.32.1.0-beta.1 (stage advances, stage_n resets)

Calendar Versioning (CalVer)

Some projects use date-based versions:

[tool.bumpversion]
current_version = "2026.03.1"
parse = '(?P<year>\d{4})\.(?P<month>\d{2})\.(?P<release>\d+)'
serialize = ["{year}.{month}.{release}"]

[tool.bumpversion.parts.year]
calver_format = "{YYYY}"

[tool.bumpversion.parts.month]
calver_format = "{0M}"

Bumping release increments the build number within the current month. Bumping month advances to the current month and resets the release number.

Advanced File Patterns

Regex Search Patterns

For files where the version string isn’t in a simple format:

[[tool.bumpversion.files]]
filename = "Chart.yaml"
search = "appVersion: {current_version}"
replace = "appVersion: {new_version}"

[[tool.bumpversion.files]]
filename = "README.md"
regex = true
search = 'version-v[\d.]+(-\w+\.\d+)?-blue'
replace = "version-v{new_version}-blue"

The regex mode is essential for version badges in README files, where the version is embedded in a URL pattern.

Glob Patterns

Update version strings across multiple files:

[[tool.bumpversion.files]]
glob = "packages/*/pyproject.toml"
search = 'version = "{current_version}"'
replace = 'version = "{new_version}"'

This is invaluable for monorepos where multiple packages share a synchronized version.

CI/CD Integration

GitHub Actions Release Pipeline

# .github/workflows/release.yml
name: Release
on:
  workflow_dispatch:
    inputs:
      bump:
        description: "Version bump type"
        required: true
        type: choice
        options:
          - patch
          - minor
          - major

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GH_PAT }}

      - uses: actions/setup-python@v5
      - run: pip install bump-my-version build twine

      - name: Configure Git
        run: |
          git config user.name "release-bot"
          git config user.email "release@example.com"

      - name: Bump version
        run: |
          bump-my-version bump ${{ inputs.bump }}
          echo "VERSION=$(bump-my-version show current_version)" >> "$GITHUB_ENV"

      - name: Push changes and tag
        run: git push origin main --tags

      - name: Build and publish to PyPI
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
        run: |
          python -m build
          twine upload dist/*

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GH_PAT }}
        run: |
          gh release create "v$VERSION" \
            --title "v$VERSION" \
            --generate-notes \
            dist/*

Using workflow_dispatch gives the release manager a button in the GitHub UI to trigger releases with a chosen bump type.

Automated Bump in Merge Pipelines

For teams that want automated bumps on every merge to main:

on:
  push:
    branches: [main]

jobs:
  auto-bump:
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, 'bump version')"
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GH_PAT }}
      - run: pip install bump-my-version
      - run: |
          git config user.name "release-bot"
          git config user.email "release@example.com"
          bump-my-version bump patch
          git push origin main --tags

The if condition prevents infinite loops — the bump commit itself won’t trigger another bump.

Hooks: Pre and Post Bump

Run scripts before or after the version bump:

[tool.bumpversion]
pre_commit_hooks = ["python scripts/validate_changelog.py"]
post_commit_hooks = ["python scripts/notify_slack.py {new_version}"]

Use pre-commit hooks to validate that the changelog has an entry for the new version, or that all tests pass. Post-commit hooks can trigger notifications, start deployment pipelines, or update external trackers.

Migration from Legacy Tools

From bump2version

bump-my-version reads .bumpversion.cfg and setup.cfg configurations. The migration path:

  1. Install bump-my-version
  2. Run bump-my-version show --config to display the interpreted configuration
  3. Move the config to pyproject.toml
  4. Remove the old .bumpversion.cfg

From Manual Versioning

For projects that have been versioning by hand, start by auditing every file that contains a version string:

grep -rn "1\.3\.2" --include="*.py" --include="*.toml" --include="*.yaml" .

Add each match to the [[tool.bumpversion.files]] configuration. Then run a dry-run bump to verify:

bump-my-version bump patch --dry-run --verbose

Integration with Other Tools

With Commitizen

Some teams use both: Commitizen for commit message enforcement and changelog generation, and bump-my-version for the actual version bump (because of its superior file pattern handling):

# In CI:
cz changelog --unreleased-version "$(bump-my-version show new_version --increment patch)"
bump-my-version bump patch

With towncrier

Generate the changelog with towncrier, then bump with bump-my-version:

towncrier build --version "$(bump-my-version show new_version --increment minor)" --yes
bump-my-version bump minor

Troubleshooting

“Version not found in file”: The search string must match exactly, including quotes and whitespace. Use --verbose --dry-run to see what bump-my-version is looking for versus what’s in the file.

Tag already exists: If a previous bump was partially applied (files changed but tag not created), you may need to git tag v1.4.0 manually or reset and re-run.

Dirty working tree: bump-my-version refuses to bump if there are uncommitted changes. Either commit or stash them first. Use --allow-dirty in development (never in CI).

One thing to remember: bump-my-version is the Swiss Army knife of version management — flexible enough for any versioning scheme, precise enough for any file format, and automatable enough for any CI pipeline.

pythonrelease-managementdeveloper-tools

See Also