diff --git a/src/python/pants/backend/python/util_rules/pex_from_targets.py b/src/python/pants/backend/python/util_rules/pex_from_targets.py
index <HASH>..<HASH> 100644
--- a/src/python/pants/backend/python/util_rules/pex_from_targets.py
+++ b/src/python/pants/backend/python/util_rules/pex_from_targets.py
@@ -206,7 +206,7 @@ async def pex_from_targets(request: PexFromTargetsRequest, python_setup: PythonS
     repository_pex: Pex | None = None
     description = request.description
 
-    if python_setup.requirement_constraints:
+    if python_setup.requirement_constraints and requirements:
         constraints_file_contents = await Get(
             DigestContents,
             PathGlobs(
@@ -288,7 +288,7 @@ async def pex_from_targets(request: PexFromTargetsRequest, python_setup: PythonS
             "`[python-setup].resolve_all_constraints` is enabled, so "
             "`[python-setup].requirement_constraints` must also be set."
         )
-    elif python_setup.lockfile:
+    elif python_setup.lockfile and requirements:
         # TODO(#12314): This does not handle the case where requirements are disjoint to the
         #  lockfile. We should likely regenerate the lockfile (or warn/error), as the inputs have
         #  changed.
Don't resolve constraints file / lockfile if no 3rd-party requirements used (#<I>)

This is an obvious performance improvement with no downsides.



[ci skip-rust]

[ci skip-build-wheels]