diff --git a/src/pip/__init__.py b/src/pip/__init__.py
index 4eff4299c01..58a3b4dc80a 100644
--- a/src/pip/__init__.py
+++ b/src/pip/__init__.py
@@ -1,6 +1,6 @@
 from typing import List, Optional
 
-__version__ = "25.0.dev0"
+__version__ = "25.0.dev0+pep-771"
 
 
 def main(args: Optional[List[str]] = None) -> int:
diff --git a/src/pip/_internal/metadata/_json.py b/src/pip/_internal/metadata/_json.py
index f3aeab3225f..227818358a7 100644
--- a/src/pip/_internal/metadata/_json.py
+++ b/src/pip/_internal/metadata/_json.py
@@ -30,6 +30,7 @@
     ("Requires-Python", False),
     ("Requires-External", True),
     ("Project-URL", True),
+    ("Default-Extra", True),
     ("Provides-Extra", True),
     ("Provides-Dist", True),
     ("Obsoletes-Dist", True),
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index 9eabcdb278b..f9d819be709 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -442,6 +442,9 @@ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requiremen
 
         For modern .dist-info distributions, this is the collection of
         "Requires-Dist:" entries in distribution metadata.
+
+        In case, no "Extra" is specified, will use "Default-Extra" as specified
+        per PEP 771.
         """
         raise NotImplementedError()
 
@@ -449,6 +452,18 @@ def iter_raw_dependencies(self) -> Iterable[str]:
         """Raw Requires-Dist metadata."""
         return self.metadata.get_all("Requires-Dist", [])
 
+    def iter_default_extras(self) -> Iterable[NormalizedName]:
+        """Extras provided by this distribution.
+
+        For modern .dist-info distributions, this is the collection of
+        "Default-Extra:" entries in distribution metadata.
+
+        The return value of this function is expected to be normalised names,
+        per PEP 771, with the returned value being handled appropriately by
+        `iter_dependencies`.
+        """
+        raise NotImplementedError()
+
     def iter_provided_extras(self) -> Iterable[NormalizedName]:
         """Extras provided by this distribution.
 
diff --git a/src/pip/_internal/metadata/importlib/_dists.py b/src/pip/_internal/metadata/importlib/_dists.py
index cf8685439a9..b81bf4d67af 100644
--- a/src/pip/_internal/metadata/importlib/_dists.py
+++ b/src/pip/_internal/metadata/importlib/_dists.py
@@ -207,6 +207,12 @@ def _metadata_impl(self) -> email.message.Message:
         # until upstream can improve the protocol. (python/cpython#94952)
         return cast(email.message.Message, self._dist.metadata)
 
+    def iter_default_extras(self) -> Iterable[NormalizedName]:
+        return [
+            canonicalize_name(extra)
+            for extra in self.metadata.get_all("Default-Extra", [])
+        ]
+
     def iter_provided_extras(self) -> Iterable[NormalizedName]:
         return [
             canonicalize_name(extra)
@@ -214,6 +220,9 @@ def iter_provided_extras(self) -> Iterable[NormalizedName]:
         ]
 
     def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
+        if not extras:
+            extras = list(self.iter_default_extras())
+
         contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]
         for req_string in self.metadata.get_all("Requires-Dist", []):
             # strip() because email.message.Message.get_all() may return a leading \n
diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py
index 4ea84f93a6f..8d97a386bdf 100644
--- a/src/pip/_internal/metadata/pkg_resources.py
+++ b/src/pip/_internal/metadata/pkg_resources.py
@@ -239,13 +239,19 @@ def _metadata_impl(self) -> email.message.Message:
         return feed_parser.close()
 
     def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
+        extras = extras or self._dist.default_extras_require
+
         if extras:
             relevant_extras = set(self._extra_mapping) & set(
                 map(canonicalize_name, extras)
             )
             extras = [self._extra_mapping[extra] for extra in relevant_extras]
+
         return self._dist.requires(extras)
 
+    def iter_default_extras(self) -> Iterable[NormalizedName]:
+        return self._dist.default_extras_require or []
+
     def iter_provided_extras(self) -> Iterable[NormalizedName]:
         return self._extra_mapping.keys()
 
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index e6aa3447200..eadebba38de 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -646,6 +646,12 @@ def _prepare_linked_requirement(
             self.build_isolation,
             self.check_build_deps,
         )
+
+        # Setting up the default-extra if necessary
+        default_extras = frozenset(dist.metadata.get_all("Default-Extra", []))
+        req.extras = req.extras or default_extras
+        req.req.extras = req.extras or default_extras
+
         return dist
 
     def save_linked_requirement(self, req: InstallRequirement) -> None:
diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py
index 6617644fe53..7f5a62b08e6 100644
--- a/src/pip/_internal/resolution/resolvelib/candidates.py
+++ b/src/pip/_internal/resolution/resolvelib/candidates.py
@@ -310,6 +310,8 @@ def __init__(
             version=version,
         )
 
+        template.extras = ireq.extras
+
     def _prepare_distribution(self) -> BaseDistribution:
         preparer = self._factory.preparer
         return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index 6c273eb88db..273140861bf 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -187,6 +187,7 @@ def _make_candidate_from_link(
         base: Optional[BaseCandidate] = self._make_base_candidate_from_link(
             link, template, name, version
         )
+        extras = extras if extras else template.extras
         if not extras or base is None:
             return base
         return self._make_extras_candidate(base, extras, comes_from=template)
@@ -482,6 +483,9 @@ def _make_requirements_from_install_req(
                 (or link) and one with the extra. This allows centralized constraint
                 handling for the base, resulting in fewer candidate rejections.
         """
+        if ireq.comes_from is not None and hasattr(ireq.comes_from, "extra"):
+            requested_extras = requested_extras or ireq.comes_from.extras
+
         if not ireq.match_markers(requested_extras):
             logger.info(
                 "Ignoring %s: markers '%s' don't match your environment",
@@ -504,6 +508,9 @@ def _make_requirements_from_install_req(
                 name=canonicalize_name(ireq.name) if ireq.name else None,
                 version=None,
             )
+
+            extras = ireq.extras or list(cand.dist.iter_default_extras())
+
             if cand is None:
                 # There's no way we can satisfy a URL requirement if the underlying
                 # candidate fails to build. An unnamed URL must be user-supplied, so
@@ -517,10 +524,10 @@ def _make_requirements_from_install_req(
             else:
                 # require the base from the link
                 yield self.make_requirement_from_candidate(cand)
-                if ireq.extras:
+                if extras:
                     # require the extras on top of the base candidate
                     yield self.make_requirement_from_candidate(
-                        self._make_extras_candidate(cand, frozenset(ireq.extras))
+                        self._make_extras_candidate(cand, frozenset(extras))
                     )
 
     def collect_root_requirements(
diff --git a/src/pip/_vendor/distlib/metadata.py b/src/pip/_vendor/distlib/metadata.py
index ce9a34b3e24..be3d4e6f60f 100644
--- a/src/pip/_vendor/distlib/metadata.py
+++ b/src/pip/_vendor/distlib/metadata.py
@@ -5,7 +5,7 @@
 #
 """Implementation of the Metadata for Python packages PEPs.
 
-Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and 2.2).
+Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1, 2.2, 2.3, 2.4 and 2.5).
 """
 from __future__ import unicode_literals
 
@@ -89,6 +89,12 @@ class MetadataInvalidError(DistlibException):
 
 _643_FIELDS = _566_FIELDS + _643_MARKERS
 
+# PEP 771
+_771_MARKERS = ('Default-Extra')
+
+_771_FIELDS = _643_FIELDS + _771_MARKERS
+
+
 _ALL_FIELDS = set()
 _ALL_FIELDS.update(_241_FIELDS)
 _ALL_FIELDS.update(_314_FIELDS)
@@ -96,6 +102,7 @@ class MetadataInvalidError(DistlibException):
 _ALL_FIELDS.update(_426_FIELDS)
 _ALL_FIELDS.update(_566_FIELDS)
 _ALL_FIELDS.update(_643_FIELDS)
+_ALL_FIELDS.update(_771_FIELDS)
 
 EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
 
@@ -115,6 +122,8 @@ def _version2fieldlist(version):
         # return _426_FIELDS
     elif version == '2.2':
         return _643_FIELDS
+    elif version == '2.5':
+        return _771_FIELDS
     raise MetadataUnrecognizedVersionError(version)
 
 
@@ -125,7 +134,7 @@ def _has_marker(keys, markers):
         return any(marker in keys for marker in markers)
 
     keys = [key for key, value in fields.items() if value not in ([], 'UNKNOWN', None)]
-    possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2']  # 2.0 removed
+    possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2', '2.5']  # 2.0 removed
 
     # first let's try to see if a field is not part of one of the version
     for key in keys:
@@ -148,6 +157,9 @@ def _has_marker(keys, markers):
         if key not in _643_FIELDS and '2.2' in possible_versions:
             possible_versions.remove('2.2')
             logger.debug('Removed 2.2 due to %s', key)
+        if key not in _771_FIELDS and '2.5' in possible_versions:
+            possible_versions.remove('2.5')
+            logger.debug('Removed 2.5 due to %s', key)
         # if key not in _426_FIELDS and '2.0' in possible_versions:
         # possible_versions.remove('2.0')
         # logger.debug('Removed 2.0 due to %s', key)
@@ -165,16 +177,19 @@ def _has_marker(keys, markers):
     is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
     # is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
     is_2_2 = '2.2' in possible_versions and _has_marker(keys, _643_MARKERS)
-    if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) > 1:
-        raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields')
+    is_2_5 = '2.5' in possible_versions and _has_marker(keys, _771_MARKERS)
+
+    if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) + int(is_2_5) > 1:
+        raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2/2.5 fields')
 
     # we have the choice, 1.0, or 1.2, 2.1 or 2.2
     #   - 1.0 has a broken Summary field but works with all tools
     #   - 1.1 is to avoid
     #   - 1.2 fixes Summary but has little adoption
     #   - 2.1 adds more features
-    #   - 2.2 is the latest
-    if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2:
+    #   - 2.2 adds more features
+    #   - 2.5 is the latest
+    if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2 and not is_2_5:
         # we couldn't find any specific marker
         if PKG_INFO_PREFERRED_VERSION in possible_versions:
             return PKG_INFO_PREFERRED_VERSION
@@ -184,10 +199,10 @@ def _has_marker(keys, markers):
         return '1.2'
     if is_2_1:
         return '2.1'
-    # if is_2_2:
-    # return '2.2'
+    if is_2_2:
+        return '2.2'
 
-    return '2.2'
+    return '2.5'
 
 
 # This follows the rules about transforming keys as described in
diff --git a/src/pip/_vendor/packaging/metadata.py b/src/pip/_vendor/packaging/metadata.py
index 721f411cfc4..5f4f7589556 100644
--- a/src/pip/_vendor/packaging/metadata.py
+++ b/src/pip/_vendor/packaging/metadata.py
@@ -132,6 +132,9 @@ class RawMetadata(TypedDict, total=False):
     license_expression: str
     license_files: list[str]
 
+    # Metadata 2.5 - PEP 771
+    default_extra: list[str]
+
 
 _STRING_FIELDS = {
     "author",
@@ -463,8 +466,8 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
 
 
 # Keep the two values in sync.
-_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
-_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
+_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
+_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
 
 _REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
 
@@ -861,3 +864,9 @@ def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:
     """``Provides`` (deprecated)"""
     obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
     """``Obsoletes`` (deprecated)"""
+    # PEP 771 lets us define a default `extras_require` if none is passed by the
+    # user.
+    default_extra: _Validator[list[utils.NormalizedName] | None] = _Validator(
+        added="2.5",
+    )
+    """:external:ref:`core-metadata-default-extra`"""
diff --git a/src/pip/_vendor/pkg_resources/__init__.py b/src/pip/_vendor/pkg_resources/__init__.py
index 57ce7f10064..0d72111d822 100644
--- a/src/pip/_vendor/pkg_resources/__init__.py
+++ b/src/pip/_vendor/pkg_resources/__init__.py
@@ -3070,6 +3070,8 @@ def requires(self, extras: Iterable[str] = ()):
         dm = self._dep_map
         deps: list[Requirement] = []
         deps.extend(dm.get(None, ()))
+
+        extras = extras or self.default_extras_require
         for ext in extras:
             try:
                 deps.extend(dm[safe_extra(ext)])
@@ -3322,6 +3324,10 @@ def clone(self, **kw: str | int | IResourceProvider | None):
     def extras(self):
         return [dep for dep in self._dep_map if dep]
 
+    @property
+    def default_extras_require(self):
+        return self._parsed_pkg_info.get_all('Default-Extra') or []
+
 
 class EggInfoDistribution(Distribution):
     def _reload_version(self):
diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py
index 2c3d0e306f9..116c19c3983 100644
--- a/src/pip/_vendor/resolvelib/resolvers.py
+++ b/src/pip/_vendor/resolvelib/resolvers.py
@@ -1,4 +1,5 @@
 import collections
+import contextlib
 import itertools
 import operator
 
@@ -172,7 +173,11 @@ def _add_to_criteria(self, criteria, requirement, parent):
         )
         if not criterion.candidates:
             raise RequirementsConflicted(criterion)
-        criteria[identifier] = criterion
+        
+        with contextlib.suppress(AttributeError):
+            requirement._extras = requirement._ireq.extras
+
+        criteria[requirement.name] = criterion
 
     def _remove_information_from_criteria(self, criteria, parents):
         """Remove information from parents of criteria.