diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py
index 8b7c02fd0..cf71b13eb 100644
--- a/src/pip/_internal/commands/list.py
+++ b/src/pip/_internal/commands/list.py
@@ -197,7 +197,7 @@ class ListCommand(Command):
                     continue
 
                 remote_version = best_candidate.version
-                if best_candidate.location.is_wheel:
+                if best_candidate.link.is_wheel:
                     typ = 'wheel'
                 else:
                     typ = 'sdist'
diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py
index f601f1831..a1aaad59c 100644
--- a/src/pip/_internal/index.py
+++ b/src/pip/_internal/index.py
@@ -477,7 +477,7 @@ def filter_unallowed_hashes(
     non_matches = []
     match_count = 0
     for candidate in candidates:
-        link = candidate.location
+        link = candidate.link
         if not link.has_hash:
             pass
         elif link.is_hash_allowed(hashes=hashes):
@@ -499,7 +499,7 @@ def filter_unallowed_hashes(
     else:
         discard_message = 'discarding {} non-matches:\n  {}'.format(
             len(non_matches),
-            '\n  '.join(str(candidate.location) for candidate in non_matches)
+            '\n  '.join(str(candidate.link) for candidate in non_matches)
         )
 
     logger.debug(
@@ -689,7 +689,7 @@ class CandidateEvaluator(object):
         support_num = len(valid_tags)
         build_tag = tuple()  # type: BuildTag
         binary_preference = 0
-        link = candidate.location
+        link = candidate.link
         if link.is_wheel:
             # can raise InvalidWheelFilename
             wheel = Wheel(link.filename)
@@ -729,7 +729,7 @@ class CandidateEvaluator(object):
         best_candidate = max(candidates, key=self._sort_key)
 
         # Log a warning per PEP 592 if necessary before returning.
-        link = best_candidate.location
+        link = best_candidate.link
         if link.is_yanked:
             reason = link.yanked_reason or '<none given>'
             msg = (
@@ -1138,7 +1138,7 @@ class PackageFinder(object):
             logger.debug(
                 'Local files found: %s',
                 ', '.join([
-                    url_to_path(candidate.location.url)
+                    url_to_path(candidate.link.url)
                     for candidate in file_versions
                 ])
             )
@@ -1265,7 +1265,7 @@ class PackageFinder(object):
             best_candidate.version,
             _format_versions(candidates.iter_applicable()),
         )
-        return best_candidate.location
+        return best_candidate.link
 
     def _get_pages(self, locations, project_name):
         # type: (Iterable[Link], str) -> Iterable[HTMLPage]
@@ -1327,7 +1327,7 @@ class PackageFinder(object):
 
         return InstallationCandidate(
             project=link_evaluator.project_name,
-            location=link,
+            link=link,
             # Convert the Text result to str since InstallationCandidate
             # accepts str.
             version=str(result),
diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py
index 3904de177..1b99690f2 100644
--- a/src/pip/_internal/models/candidate.py
+++ b/src/pip/_internal/models/candidate.py
@@ -13,24 +13,24 @@ class InstallationCandidate(KeyBasedCompareMixin):
     """Represents a potential "candidate" for installation.
     """
 
-    def __init__(self, project, version, location):
+    def __init__(self, project, version, link):
         # type: (Any, str, Link) -> None
         self.project = project
         self.version = parse_version(version)  # type: _BaseVersion
-        self.location = location
+        self.link = link
 
         super(InstallationCandidate, self).__init__(
-            key=(self.project, self.version, self.location),
+            key=(self.project, self.version, self.link),
             defining_class=InstallationCandidate
         )
 
     def __repr__(self):
         # type: () -> str
         return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
-            self.project, self.version, self.location,
+            self.project, self.version, self.link,
         )
 
     def __str__(self):
         return '{!r} candidate (version {} at {})'.format(
-            self.project, self.version, self.location,
+            self.project, self.version, self.link,
         )

Rename InstallationCandidate.{location -> link}
In `pip._internal.models.candidate`.

It's more representative of what it really is -- "location" caused confusion whether it's on disk or not. It's not, so `link` is a better name for it IMO.
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index 8810f30b9..0e0c41081 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -262,8 +262,8 @@ def test_finder_priority_file_over_page(data):
     )
     all_versions = finder.find_all_candidates(req.name)
     # 1 file InstallationCandidate followed by all https ones
-    assert all_versions[0].location.scheme == 'file'
-    assert all(version.location.scheme == 'https'
+    assert all_versions[0].link.scheme == 'file'
+    assert all(version.link.scheme == 'https'
                for version in all_versions[1:]), all_versions
 
     link = finder.find_requirement(req, False)
@@ -279,8 +279,8 @@ def test_finder_priority_nonegg_over_eggfragments():
 
     with patch.object(finder, "_get_pages", lambda x, y: []):
         all_versions = finder.find_all_candidates(req.name)
-        assert all_versions[0].location.url.endswith('tar.gz')
-        assert all_versions[1].location.url.endswith('#egg=bar-1.0')
+        assert all_versions[0].link.url.endswith('tar.gz')
+        assert all_versions[1].link.url.endswith('#egg=bar-1.0')
 
         link = finder.find_requirement(req, False)
 
@@ -291,8 +291,8 @@ def test_finder_priority_nonegg_over_eggfragments():
 
     with patch.object(finder, "_get_pages", lambda x, y: []):
         all_versions = finder.find_all_candidates(req.name)
-        assert all_versions[0].location.url.endswith('tar.gz')
-        assert all_versions[1].location.url.endswith('#egg=bar-1.0')
+        assert all_versions[0].link.url.endswith('tar.gz')
+        assert all_versions[1].link.url.endswith('#egg=bar-1.0')
         link = finder.find_requirement(req, False)
 
     assert link.url.endswith('tar.gz')
diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py
index ab9a2c9dd..c922dc773 100644
--- a/tests/unit/test_models.py
+++ b/tests/unit/test_models.py
@@ -49,7 +49,7 @@ class TestInstallationCandidate(object):
         )
         assert obj.project == "A"
         assert obj.version == parse_version("1.0.0")
-        assert obj.location == "https://somewhere.com/path/A-1.0.0.tar.gz"
+        assert obj.link == "https://somewhere.com/path/A-1.0.0.tar.gz"
 
     # NOTE: This isn't checking the ordering logic; only the data provided to
     #       it is correct.
@@ -57,4 +57,4 @@ class TestInstallationCandidate(object):
         obj = candidate.InstallationCandidate(
             "A", "1.0.0", "https://somewhere.com/path/A-1.0.0.tar.gz"
         )
-        assert obj._compare_key == (obj.project, obj.version, obj.location)
+        assert obj._compare_key == (obj.project, obj.version, obj.link)

pip install -e .
parse_log_pytest
pytest
pytest pytest-cov pytest-rerunfailures pytest-timeout pytest-xdist freezegun mock pretend pyyaml setuptools>=39.2.0 scripttest virtualenv wheel
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.8.0
distlib==0.3.9
exceptiongroup==1.2.0
execnet==2.1.1
filelock==3.18.0
freezegun==1.5.1
iniconfig==1.1.1
mock==5.2.0
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
pretend==1.0.9
pytest==8.3.4
pytest-cov==6.0.0
pytest-rerunfailures==15.0
pytest-timeout==2.3.1
pytest-xdist==3.6.1
python-dateutil==2.9.0.post0
PyYAML==6.0.2
scripttest==2.0
six==1.17.0
tomli==2.0.1
virtualenv==20.29.3

name: pip
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
  - 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:
      - coverage==7.8.0
      - distlib==0.3.9
      - execnet==2.1.1
      - filelock==3.18.0
      - freezegun==1.5.1
      - mock==5.2.0
      - pip==19.2.dev0
      - platformdirs==4.3.7
      - pretend==1.0.9
      - pytest-cov==6.0.0
      - pytest-rerunfailures==15.0
      - pytest-timeout==2.3.1
      - pytest-xdist==3.6.1
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - scripttest==2.0
      - six==1.17.0
      - virtualenv==20.29.3
prefix: /opt/conda/envs/pip

tests/unit/test_finder.py::test_finder_priority_file_over_page tests/unit/test_finder.py::test_finder_priority_nonegg_over_eggfragments tests/unit/test_models.py::TestInstallationCandidate::test_sets_correct_variables tests/unit/test_models.py::TestInstallationCandidate::test_sets_the_right_key
tests/unit/test_finder.py::test_no_mpkg tests/unit/test_finder.py::test_no_partial_name_match tests/unit/test_finder.py::test_tilde tests/unit/test_finder.py::test_duplicates_sort_ok tests/unit/test_finder.py::test_finder_detects_latest_find_links tests/unit/test_finder.py::test_incorrect_case_file_index tests/unit/test_finder.py::test_finder_detects_latest_already_satisfied_find_links tests/unit/test_finder.py::test_finder_detects_latest_already_satisfied_pypi_links tests/unit/test_finder.py::TestWheel::test_skip_invalid_wheel_link tests/unit/test_finder.py::TestWheel::test_not_find_wheel_not_supported tests/unit/test_finder.py::TestWheel::test_find_wheel_supported tests/unit/test_finder.py::TestWheel::test_wheel_over_sdist_priority tests/unit/test_finder.py::TestWheel::test_existing_over_wheel_priority tests/unit/test_finder.py::TestWheel::test_link_sorting tests/unit/test_finder.py::TestWheel::test_link_sorting_wheels_with_build_tags tests/unit/test_finder.py::test_finder_only_installs_stable_releases tests/unit/test_finder.py::test_finder_only_installs_data_require tests/unit/test_finder.py::test_finder_installs_pre_releases tests/unit/test_finder.py::test_finder_installs_dev_releases tests/unit/test_finder.py::test_finder_installs_pre_releases_with_version_spec tests/unit/test_finder.py::TestLinkEvaluator::test_evaluate_link__match[http:/yo/pytest-1.0.tar.gz-1.0] tests/unit/test_finder.py::TestLinkEvaluator::test_evaluate_link__match[http:/yo/pytest-1.0-py2.py3-none-any.whl-1.0] tests/unit/test_finder.py::TestLinkEvaluator::test_evaluate_link__substring_fails[http:/yo/pytest2-1.0.tar.gz-Missing tests/unit/test_finder.py::TestLinkEvaluator::test_evaluate_link__substring_fails[http:/yo/pytest_xdist-1.0-py2.py3-none-any.whl-wrong tests/unit/test_finder.py::test_find_all_candidates_nothing tests/unit/test_finder.py::test_find_all_candidates_find_links tests/unit/test_finder.py::test_find_all_candidates_index tests/unit/test_finder.py::test_find_all_candidates_find_links_and_index tests/unit/test_models.py::TestPackageIndex::test_gives_right_urls tests/unit/test_models.py::TestPackageIndex::test_PyPI_urls_are_correct tests/unit/test_models.py::TestPackageIndex::test_TestPyPI_urls_are_correct
src/pip/_internal/index.py src/pip/_internal/models/candidate.py src/pip/_internal/commands/list.py
src/pip/_internal/index.py src/pip/_internal/models/candidate.py src/pip/_internal/commands/list.py