diff --git a/bumpversion/exceptions.py b/bumpversion/exceptions.py
index ecce8b6..a635316 100644
--- a/bumpversion/exceptions.py
+++ b/bumpversion/exceptions.py
@@ -1,18 +1,28 @@
-class IncompleteVersionRepresentationException(Exception):
+
+
+class BumpVersionException(Exception):
+    """Custom base class for all BumpVersion exception types."""
+
+
+class IncompleteVersionRepresentationException(BumpVersionException):
     def __init__(self, message):
         self.message = message
 
 
-class MissingValueForSerializationException(Exception):
+class MissingValueForSerializationException(BumpVersionException):
     def __init__(self, message):
         self.message = message
 
 
-class WorkingDirectoryIsDirtyException(Exception):
+class WorkingDirectoryIsDirtyException(BumpVersionException):
     def __init__(self, message):
         self.message = message
 
 
-class MercurialDoesNotSupportSignedTagsException(Exception):
+class MercurialDoesNotSupportSignedTagsException(BumpVersionException):
     def __init__(self, message):
         self.message = message
+
+
+class VersionNotFoundException(BumpVersionException):
+    """A version number was not found in a source file."""
diff --git a/bumpversion/utils.py b/bumpversion/utils.py
index 4f5be53..f872f23 100644
--- a/bumpversion/utils.py
+++ b/bumpversion/utils.py
@@ -4,6 +4,7 @@ import io
 import logging
 import os
 
+from bumpversion.exceptions import VersionNotFoundException
 
 logger = logging.getLogger(__name__)
 
@@ -33,30 +34,47 @@ def prefixed_environ():
 
 
 class ConfiguredFile:
+
     def __init__(self, path, versionconfig):
         self.path = path
         self._versionconfig = versionconfig
 
     def should_contain_version(self, version, context):
+        """
+        Raise VersionNotFound if the version number isn't present in this file.
 
+        Return normally if the version number is in fact present.
+        """
         context["current_version"] = self._versionconfig.serialize(version, context)
+        search_expression = self._versionconfig.search.format(**context)
 
-        serialized_version = self._versionconfig.search.format(**context)
-
-        if self.contains(serialized_version):
+        if self.contains(search_expression):
             return
 
-        msg = "Did not find '{}' or '{}' in file {}".format(
-            version.original, serialized_version, self.path
-        )
+        # the `search` pattern did not match, but the original supplied
+        # version number (representing the same version part values) might
+        # match instead.
+
+        # check whether `search` isn't customized, i.e. should match only
+        # very specific parts of the file
+        search_pattern_is_default = self._versionconfig.search == "{current_version}"
 
-        if version.original:
-            assert self.contains(version.original), msg
+        if search_pattern_is_default and self.contains(version.original):
+            # original version is present and we're not looking for something
+            # more specific -> this is accepted as a match
             return
 
-        assert False, msg
+        # version not found
+        raise VersionNotFoundException(
+            "Did not find '{}' in file: '{}'".format(
+                search_expression, self.path
+            )
+        )
 
     def contains(self, search):
+        if not search:
+            return False
+
         with open(self.path, "rt", encoding="utf-8") as f:
             search_lines = search.splitlines()
             lookbehind = []

Don't fall back to original_version if `search` pattern is not found
This is one of the areas where bump2version does not work like I expect it to.

If for any reason (user error) the `search` pattern is not found, it simply assumes that it can replace `{current_version}` with the new version.

This can lead to incorrect changes (broken files!) as demonstrated in #126.  I often encounter this while developing `search` patterns and when using custom `replace` patterns.

I think this 'fall-back feature' does more harm than good, and we should drop it.

Given a file version.txt

    release = "1.0.0"

And a config

    [bumpversion]
    current_version = 1.0.0

    [bumpversion:file:version.txt]
    search = current_release = "{current_version}"
    replace = current_release = "{new_version}"

Doing `bumpversion patch` produces this without any warning:

    release = "current_release = 1.0.1"

This is *wrong*!
diff --git a/tests/test_cli.py b/tests/test_cli.py
index bf912f9..71ef3a0 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -15,7 +15,7 @@ import pytest
 from testfixtures import LogCapture
 
 import bumpversion
-from bumpversion.exceptions import WorkingDirectoryIsDirtyException
+from bumpversion import exceptions
 from bumpversion.cli import DESCRIPTION, main, split_args_in_optional_and_positional
 
 
@@ -169,7 +169,7 @@ def test_usage_string(tmpdir, capsys):
     assert EXPECTED_USAGE in out
 
 
-def test_usage_string_fork(tmpdir, capsys):
+def test_usage_string_fork(tmpdir):
     tmpdir.chdir()
 
     if platform.system() == "Windows":
@@ -408,7 +408,7 @@ def test_dirty_work_dir(tmpdir, vcs):
     vcs_name = "Mercurial" if vcs == "hg" else "Git"
     vcs_output = "A dirty" if vcs == "hg" else "A  dirty"
 
-    with pytest.raises(WorkingDirectoryIsDirtyException):
+    with pytest.raises(exceptions.WorkingDirectoryIsDirtyException):
         with LogCapture() as log_capture:
             main(['patch', '--current-version', '1', '--new-version', '2', 'file7'])
 
@@ -746,7 +746,7 @@ def test_tag_name(tmpdir, vcs):
     assert b'ReleasedVersion-31.1.2' in tag_out
 
 
-def test_message_from_config_file(tmpdir, capsys, vcs):
+def test_message_from_config_file(tmpdir, vcs):
     tmpdir.chdir()
     check_call([vcs, "init"])
     tmpdir.join("VERSION").write("400.0.0")
@@ -772,7 +772,7 @@ tag_name: from-{current_version}-to-{new_version}""")
     assert b'from-400.0.0-to-401.0.0' in tag_out
 
 
-def test_all_parts_in_message_and_serialize_and_tag_name_from_config_file(tmpdir, capsys, vcs):
+def test_all_parts_in_message_and_serialize_and_tag_name_from_config_file(tmpdir, vcs):
     """
     Ensure that major/minor/patch *and* custom parts can be used everywhere.
 
@@ -811,7 +811,7 @@ tag_name: from-{current_version}-aka-{current_major}.{current_minor}.{current_pa
     assert b'from-400.1.2.101-aka-400.1.2-custom-101-to-401.2.3.102-aka-401.2.3-custom-102' in tag_out
 
 
-def test_all_parts_in_replace_from_config_file(tmpdir, capsys, vcs):
+def test_all_parts_in_replace_from_config_file(tmpdir, vcs):
     """
     Ensure that major/minor/patch *and* custom parts can be used in 'replace'.
     """
@@ -931,7 +931,7 @@ except ImportError:
 
 @pytest.mark.xfail(not config_parser_handles_utf8,
                    reason="old ConfigParser uses non-utf-8-strings internally")
-def test_utf8_message_from_config_file(tmpdir, capsys, vcs):
+def test_utf8_message_from_config_file(tmpdir, vcs):
     tmpdir.chdir()
     check_call([vcs, "init"])
     tmpdir.join("VERSION").write("500.0.0")
@@ -952,7 +952,7 @@ message = Nová verze: {current_version} ☃, {new_version} ☀
     assert expected_new_config.encode('utf-8') == tmpdir.join(".bumpversion.cfg").read(mode='rb')
 
 
-def test_utf8_message_from_config_file(tmpdir, capsys, vcs):
+def test_utf8_message_from_config_file(tmpdir, vcs):
     tmpdir.chdir()
     check_call([vcs, "init"])
     tmpdir.join("VERSION").write("10.10.0")
@@ -1082,7 +1082,7 @@ parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+))?
     assert '0.6.1' == tmpdir.join("fileD").read()
 
 
-def test_log_no_config_file_info_message(tmpdir, capsys):
+def test_log_no_config_file_info_message(tmpdir):
     tmpdir.chdir()
 
     tmpdir.join("a_file.txt").write("1.0.0")
@@ -1140,7 +1140,7 @@ def test_log_invalid_regex_exit(tmpdir):
     )
 
 
-def test_complex_info_logging(tmpdir, capsys):
+def test_complex_info_logging(tmpdir):
     tmpdir.join("fileE").write("0.4")
     tmpdir.chdir()
 
@@ -1337,7 +1337,7 @@ def test_no_list_no_stdout(tmpdir, vcs):
     assert out == ""
 
 
-def test_bump_non_numeric_parts(tmpdir, capsys):
+def test_bump_non_numeric_parts(tmpdir):
     tmpdir.join("with_pre_releases.txt").write("1.5.dev")
     tmpdir.chdir()
 
@@ -1397,7 +1397,7 @@ def test_optional_value_from_documentation(tmpdir):
     assert '1' == tmpdir.join("optional_value_from_doc.txt").read()
 
 
-def test_python_pre_release_release_post_release(tmpdir, capsys):
+def test_python_pre_release_release_post_release(tmpdir):
     tmpdir.join("python386.txt").write("1.0a")
     tmpdir.chdir()
 
@@ -1479,7 +1479,7 @@ def test_part_first_value(tmpdir):
     assert '1.1.0' == tmpdir.join("the_version.txt").read()
 
 
-def test_multi_file_configuration(tmpdir, capsys):
+def test_multi_file_configuration(tmpdir):
     tmpdir.join("FULL_VERSION.txt").write("1.0.3")
     tmpdir.join("MAJOR_VERSION.txt").write("1")
 
@@ -1506,7 +1506,7 @@ def test_multi_file_configuration(tmpdir, capsys):
     assert '2' in tmpdir.join("MAJOR_VERSION.txt").read()
 
 
-def test_multi_file_configuration2(tmpdir, capsys):
+def test_multi_file_configuration2(tmpdir):
     tmpdir.join("setup.cfg").write("1.6.6")
     tmpdir.join("README.txt").write("MyAwesomeSoftware(TM) v1.6")
     tmpdir.join("BUILD_NUMBER").write("1.6.6+joe+38943")
@@ -1555,7 +1555,7 @@ def test_multi_file_configuration2(tmpdir, capsys):
     assert '1.7.1+bob+38945' in tmpdir.join("BUILD_NUMBER").read()
 
 
-def test_search_replace_to_avoid_updating_unconcerned_lines(tmpdir, capsys):
+def test_search_replace_to_avoid_updating_unconcerned_lines(tmpdir):
     tmpdir.chdir()
 
     tmpdir.join("requirements.txt").write("Django>=1.5.6,<1.6\nMyProject==1.5.6")
@@ -1596,7 +1596,7 @@ def test_search_replace_to_avoid_updating_unconcerned_lines(tmpdir, capsys):
     assert 'Django>=1.5.6' in tmpdir.join("requirements.txt").read()
 
 
-def test_search_replace_expanding_changelog(tmpdir, capsys):
+def test_search_replace_expanding_changelog(tmpdir):
     tmpdir.chdir()
 
     tmpdir.join("CHANGELOG.md").write(dedent("""
@@ -1654,7 +1654,42 @@ def test_search_replace_expanding_changelog(tmpdir, capsys):
     assert postdate in tmpdir.join("CHANGELOG.md").read()
 
 
-def test_search_replace_cli(tmpdir, capsys):
+def test_non_matching_search_does_not_modify_file(tmpdir):
+    tmpdir.chdir()
+
+    changelog_content = dedent("""
+    # Unreleased
+    
+    * bullet point A
+    
+    # Release v'older' (2019-09-17)
+    
+    * bullet point B
+    """)
+
+    config_content = dedent("""
+      [bumpversion]
+      current_version = 1.0.3
+
+      [bumpversion:file:CHANGELOG.md]
+      search = Not-yet-released
+      replace = Release v{new_version} ({now:%Y-%m-%d})
+    """)
+
+    tmpdir.join("CHANGELOG.md").write(changelog_content)
+    tmpdir.join(".bumpversion.cfg").write(config_content)
+
+    with pytest.raises(
+            exceptions.VersionNotFoundException,
+            match="Did not find 'Not-yet-released' in file: 'CHANGELOG.md'"
+    ):
+        main(['patch', '--verbose'])
+
+    assert changelog_content == tmpdir.join("CHANGELOG.md").read()
+    assert config_content in tmpdir.join(".bumpversion.cfg").read()
+
+
+def test_search_replace_cli(tmpdir):
     tmpdir.join("file89").write("My birthday: 3.5.98\nCurrent version: 3.5.98")
     tmpdir.chdir()
     main([
@@ -1829,7 +1864,7 @@ def test_configparser_empty_lines_in_values(tmpdir):
     """) == tmpdir.join("CHANGES.rst").read()
 
 
-def test_regression_tag_name_with_hyphens(tmpdir, capsys, git):
+def test_regression_tag_name_with_hyphens(tmpdir, git):
     tmpdir.chdir()
     tmpdir.join("some_source.txt").write("2014.10.22")
     check_call([git, "init"])
@@ -1873,7 +1908,7 @@ message = XXX
     assert "Failed to run" in caplog.text
 
 
-def test_regression_characters_after_last_label_serialize_string(tmpdir, capsys):
+def test_regression_characters_after_last_label_serialize_string(tmpdir):
     tmpdir.chdir()
     tmpdir.join("bower.json").write('''
     {
@@ -1894,7 +1929,7 @@ def test_regression_characters_after_last_label_serialize_string(tmpdir, capsys)
     main(['patch', 'bower.json'])
 
 
-def test_regression_dont_touch_capitalization_of_keys_in_config(tmpdir, capsys):
+def test_regression_dont_touch_capitalization_of_keys_in_config(tmpdir):
     tmpdir.chdir()
     tmpdir.join("setup.cfg").write(dedent("""
     [bumpversion]
@@ -1915,7 +1950,7 @@ def test_regression_dont_touch_capitalization_of_keys_in_config(tmpdir, capsys):
     """).strip() == tmpdir.join("setup.cfg").read().strip()
 
 
-def test_regression_new_version_cli_in_files(tmpdir, capsys):
+def test_regression_new_version_cli_in_files(tmpdir):
     """
     Reported here: https://github.com/peritus/bumpversion/issues/60
     """

pip install -e .
parse_log_pytest
pytest
pytest>=3.4.0 testfixtures pytest
apt-get update apt-get install -y git-core mercurial
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
-e git+https://github.com/c4urself/bump2version.git@f3d0035176452f990f57c44339d344065f11f21c#egg=bump2version
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
testfixtures==8.3.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: bump2version
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - testfixtures==8.3.0
prefix: /opt/conda/envs/bump2version

tests/test_cli.py::test_non_matching_search_does_not_modify_file
tests/test_cli.py::test_usage_string tests/test_cli.py::test_regression_help_in_work_dir[git] tests/test_cli.py::test_regression_help_in_work_dir[hg] tests/test_cli.py::test_defaults_in_usage_with_config
tests/test_cli.py::test_usage_string_fork tests/test_cli.py::test_missing_explicit_config_file tests/test_cli.py::test_simple_replacement tests/test_cli.py::test_simple_replacement_in_utf8_file tests/test_cli.py::test_config_file tests/test_cli.py::test_default_config_files[.bumpversion.cfg] tests/test_cli.py::test_default_config_files[setup.cfg] tests/test_cli.py::test_multiple_config_files tests/test_cli.py::test_config_file_is_updated tests/test_cli.py::test_dry_run[git] tests/test_cli.py::test_dry_run[hg] tests/test_cli.py::test_bump_version tests/test_cli.py::test_bump_version_custom_main tests/test_cli.py::test_bump_version_custom_parse_serialize_configfile tests/test_cli.py::test_bumpversion_custom_parse_semver tests/test_cli.py::test_dirty_work_dir[git] tests/test_cli.py::test_dirty_work_dir[hg] tests/test_cli.py::test_force_dirty_work_dir[git] tests/test_cli.py::test_force_dirty_work_dir[hg] tests/test_cli.py::test_bump_major tests/test_cli.py::test_commit_and_tag[git] tests/test_cli.py::test_commit_and_tag[hg] tests/test_cli.py::test_commit_and_tag_with_configfile[git] tests/test_cli.py::test_commit_and_tag_with_configfile[hg] tests/test_cli.py::test_commit_and_not_tag_with_configfile[git-[bumpversion]\ncommit tests/test_cli.py::test_commit_and_not_tag_with_configfile[hg-[bumpversion]\ncommit tests/test_cli.py::test_commit_explicitly_false[git] tests/test_cli.py::test_commit_explicitly_false[hg] tests/test_cli.py::test_commit_configfile_true_cli_false_override[git] tests/test_cli.py::test_commit_configfile_true_cli_false_override[hg] tests/test_cli.py::test_bump_version_environment tests/test_cli.py::test_current_version_from_tag[git] tests/test_cli.py::test_current_version_from_tag_written_to_config_file[git] tests/test_cli.py::test_distance_to_latest_tag_as_part_of_new_version[git] tests/test_cli.py::test_override_vcs_current_version[git] tests/test_cli.py::test_non_existing_file tests/test_cli.py::test_non_existing_second_file tests/test_cli.py::test_read_version_tags_only[git] tests/test_cli.py::test_tag_name[git] tests/test_cli.py::test_tag_name[hg] tests/test_cli.py::test_message_from_config_file[git] tests/test_cli.py::test_message_from_config_file[hg] tests/test_cli.py::test_all_parts_in_message_and_serialize_and_tag_name_from_config_file[git] tests/test_cli.py::test_all_parts_in_message_and_serialize_and_tag_name_from_config_file[hg] tests/test_cli.py::test_all_parts_in_replace_from_config_file[git] tests/test_cli.py::test_all_parts_in_replace_from_config_file[hg] tests/test_cli.py::test_unannotated_tag[git] tests/test_cli.py::test_unannotated_tag[hg] tests/test_cli.py::test_annotated_tag[git] tests/test_cli.py::test_annotated_tag[hg] tests/test_cli.py::test_vcs_describe[git] tests/test_cli.py::test_utf8_message_from_config_file[git] tests/test_cli.py::test_utf8_message_from_config_file[hg] tests/test_cli.py::test_commit_and_tag_from_below_vcs_root[git] tests/test_cli.py::test_commit_and_tag_from_below_vcs_root[hg] tests/test_cli.py::test_non_vcs_operations_if_vcs_is_not_installed[git] tests/test_cli.py::test_non_vcs_operations_if_vcs_is_not_installed[hg] tests/test_cli.py::test_serialize_newline tests/test_cli.py::test_multiple_serialize_three_part tests/test_cli.py::test_multiple_serialize_two_part tests/test_cli.py::test_multiple_serialize_two_part_patch tests/test_cli.py::test_multiple_serialize_two_part_patch_configfile tests/test_cli.py::test_log_no_config_file_info_message tests/test_cli.py::test_log_parse_doesnt_parse_current_version tests/test_cli.py::test_log_invalid_regex_exit tests/test_cli.py::test_complex_info_logging tests/test_cli.py::test_subjunctive_dry_run_logging[git] tests/test_cli.py::test_subjunctive_dry_run_logging[hg] tests/test_cli.py::test_log_commit_message_if_no_commit_tag_but_usable_vcs[git] tests/test_cli.py::test_log_commit_message_if_no_commit_tag_but_usable_vcs[hg] tests/test_cli.py::test_listing[git] tests/test_cli.py::test_listing[hg] tests/test_cli.py::test_no_list_no_stdout[git] tests/test_cli.py::test_no_list_no_stdout[hg] tests/test_cli.py::test_bump_non_numeric_parts tests/test_cli.py::test_optional_value_from_documentation tests/test_cli.py::test_python_pre_release_release_post_release tests/test_cli.py::test_part_first_value tests/test_cli.py::test_multi_file_configuration tests/test_cli.py::test_multi_file_configuration2 tests/test_cli.py::test_search_replace_to_avoid_updating_unconcerned_lines tests/test_cli.py::test_search_replace_expanding_changelog tests/test_cli.py::test_search_replace_cli tests/test_cli.py::test_deprecation_warning_files_in_global_configuration tests/test_cli.py::test_deprecation_warning_multiple_files_cli tests/test_cli.py::test_file_specific_config_inherits_parse_serialize tests/test_cli.py::test_multi_line_search_is_found tests/test_cli.py::test_configparser_empty_lines_in_values tests/test_cli.py::test_regression_tag_name_with_hyphens[git] tests/test_cli.py::test_unclean_repo_exception[git] tests/test_cli.py::test_regression_characters_after_last_label_serialize_string tests/test_cli.py::test_regression_dont_touch_capitalization_of_keys_in_config tests/test_cli.py::test_regression_new_version_cli_in_files tests/test_cli.py::test_correct_interpolation_for_setup_cfg_files[.bumpversion.cfg] tests/test_cli.py::test_correct_interpolation_for_setup_cfg_files[setup.cfg] tests/test_cli.py::test_retain_newline[.bumpversion.cfg-\n] tests/test_cli.py::test_retain_newline[.bumpversion.cfg-\r\n] tests/test_cli.py::test_retain_newline[setup.cfg-\n] tests/test_cli.py::test_retain_newline[setup.cfg-\r\n] tests/test_cli.py::TestSplitArgsInOptionalAndPositional::test_all_optional tests/test_cli.py::TestSplitArgsInOptionalAndPositional::test_all_positional tests/test_cli.py::TestSplitArgsInOptionalAndPositional::test_no_args tests/test_cli.py::TestSplitArgsInOptionalAndPositional::test_short_optionals tests/test_cli.py::TestSplitArgsInOptionalAndPositional::test_1optional_2positional tests/test_cli.py::TestSplitArgsInOptionalAndPositional::test_2optional_1positional tests/test_cli.py::TestSplitArgsInOptionalAndPositional::test_2optional_mixed_2positional
bumpversion/utils.py bumpversion/exceptions.py
bumpversion/utils.py bumpversion/exceptions.py