diff --git a/docs/html/cli/index.md b/docs/html/cli/index.md
index a3497c308c2..4b56dbeb4cb 100644
--- a/docs/html/cli/index.md
+++ b/docs/html/cli/index.md
@@ -16,6 +16,7 @@ pip
pip_install
pip_uninstall
+pip_inspect
pip_list
pip_show
pip_freeze
diff --git a/docs/html/cli/pip_inspect.rst b/docs/html/cli/pip_inspect.rst
new file mode 100644
index 00000000000..66217735ed1
--- /dev/null
+++ b/docs/html/cli/pip_inspect.rst
@@ -0,0 +1,32 @@
+.. _`pip inspect`:
+
+===========
+pip inspect
+===========
+
+
+
+Usage
+=====
+
+.. tab:: Unix/macOS
+
+ .. pip-command-usage:: inspect "python -m pip"
+
+.. tab:: Windows
+
+ .. pip-command-usage:: inspect "py -m pip"
+
+
+Description
+===========
+
+.. pip-command-description:: inspect
+
+The format of the JSON output is described in :doc:`../reference/inspect-report`.
+
+
+Options
+=======
+
+.. pip-command-options:: inspect
diff --git a/docs/html/reference/index.md b/docs/html/reference/index.md
index 855dc79b37a..8b199a4f2d2 100644
--- a/docs/html/reference/index.md
+++ b/docs/html/reference/index.md
@@ -9,4 +9,5 @@ interoperability standards that pip utilises/implements.
build-system/index
requirement-specifiers
requirements-file-format
+inspect-report
```
diff --git a/docs/html/reference/inspect-report.md b/docs/html/reference/inspect-report.md
new file mode 100644
index 00000000000..e482741da89
--- /dev/null
+++ b/docs/html/reference/inspect-report.md
@@ -0,0 +1,79 @@
+# `pip inspect` JSON output specification
+
+The `pip inspect` command produces a detailed JSON report of the Python
+environment, including installed distributions.
+
+## Specification
+
+The report is a JSON object with the following properties:
+
+- `version`: the string `0`, denoting that the inspect command is an experimental
+ feature. This value will change to `1`, when the feature is deemed stable after
+ gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes
+ may be introduced in version `1` without notice. After that, it will change only if
+ and when backward incompatible changes are introduced, such as removing mandatory
+ fields or changing the semantics or data type of existing fields. The introduction of
+ backward incompatible changes will follow the usual pip processes such as the
+ deprecation cycle or feature flags. Tools must check this field to ensure they support
+ the corresponding version.
+
+- `pip_version`: a string with the version of pip used to produce the report.
+
+- `installed`: an array of `InspectReportItem` (see below) representing the
+ distribution packages that are installed.
+
+- `environment`: an object describing the environment where the installation report was
+ generated. See [PEP 508 environment
+ markers](https://peps.python.org/pep-0508/#environment-markers) for more information.
+ Values have a string type.
+
+An `InspectReportItem` is an object describing a (to be) installed distribution
+package with the following properties:
+
+- `metadata`: the metadata of the distribution, converted to a JSON object according to
+ the [PEP 566
+ transformation](https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata).
+
+- `metadata_location`: the location of the metadata of the installed distribution. Most
+ of the time this is the `.dist-info` directory. For legacy installs it is the
+ `.egg-info` directory.
+
+ ```{warning}
+ This field may not necessary point to a directory, for instance, in the case of older
+ `.egg` installs.
+ ```
+
+- `direct_url`: Information about the direct URL that was used for installation, if any,
+ using the [direct
+ URL](https://packaging.python.org/en/latest/specifications/direct-url/) data
+ structure. In most case, this field the `direct_url.json` metadata, except for legacy
+ editable installs, where it is emulated.
+
+- `requested`: `true` if the `REQUESTED` metadata is present, `false` otherwise. This
+ field is only present for modern `.dist-info` installations.
+
+ ```{note}
+ The `REQUESTED` metadata may not be generated by all installers.
+ It is generated by pip since version 20.2.
+ ```
+
+- `installer`: the content of the `INSTALLER` metadata, if present and not empty.
+
+## Example
+
+The following command:
+
+```console
+pip inspect
+```
+
+will produce an output similar to this (metadata abriged for brevity):
+
+```json
+{
+ "version": "0",
+ "pip_version": "22.2",
+ "installed": [],
+ "environment": {}
+}
+```
diff --git a/news/11245.feature.rst b/news/11245.feature.rst
new file mode 100644
index 00000000000..ba80a8976da
--- /dev/null
+++ b/news/11245.feature.rst
@@ -0,0 +1,2 @@
+Add ``pip inspect`` command to obtain the list of installed distributions and other
+information about the Python environment, in JSON format.
diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py
index c72f24f30e2..858a4101416 100644
--- a/src/pip/_internal/commands/__init__.py
+++ b/src/pip/_internal/commands/__init__.py
@@ -38,6 +38,11 @@
"FreezeCommand",
"Output installed packages in requirements format.",
),
+ "inspect": CommandInfo(
+ "pip._internal.commands.inspect",
+ "InspectCommand",
+ "Inspect the python environment.",
+ ),
"list": CommandInfo(
"pip._internal.commands.list",
"ListCommand",
diff --git a/src/pip/_internal/commands/inspect.py b/src/pip/_internal/commands/inspect.py
new file mode 100644
index 00000000000..975b2b5db33
--- /dev/null
+++ b/src/pip/_internal/commands/inspect.py
@@ -0,0 +1,91 @@
+from optparse import Values
+from typing import Any, Dict, List
+
+from pip._vendor import rich
+from pip._vendor.packaging.markers import default_environment
+from pip._vendor.rich.json import JSON
+
+from pip import __version__
+from pip._internal.cli import cmdoptions
+from pip._internal.cli.req_command import Command
+from pip._internal.cli.status_codes import SUCCESS
+from pip._internal.metadata import BaseDistribution, get_environment
+from pip._internal.utils.compat import stdlib_pkgs
+from pip._internal.utils.urls import path_to_url
+
+
+class InspectCommand(Command):
+ """
+ Inspect the content of a Python environment.
+ """
+
+ ignore_require_venv = True
+ usage = """
+ %prog [options]"""
+
+ def add_options(self) -> None:
+ self.cmd_opts.add_option(
+ "-l",
+ "--local",
+ action="store_true",
+ default=False,
+ help=(
+ "If in a virtualenv that has global access, do not list "
+ "globally-installed packages."
+ ),
+ )
+ self.cmd_opts.add_option(
+ "--user",
+ dest="user",
+ action="store_true",
+ default=False,
+ help="Only output packages installed in user-site.",
+ )
+ self.cmd_opts.add_option(cmdoptions.list_path())
+ self.parser.insert_option_group(0, self.cmd_opts)
+
+ def run(self, options: Values, args: List[str]) -> int:
+ cmdoptions.check_list_path_option(options)
+ dists = get_environment(options.path).iter_installed_distributions(
+ local_only=options.local,
+ user_only=options.user,
+ skip=set(stdlib_pkgs),
+ )
+ output = {
+ "version": "0",
+ "pip_version": __version__,
+ "installed": [self._dist_to_dict(dist) for dist in dists],
+ "environment": default_environment(),
+ # TODO tags? scheme?
+ }
+ rich.print(JSON.from_data(output))
+ return SUCCESS
+
+ def _dist_to_dict(self, dist: BaseDistribution) -> Dict[str, Any]:
+ res: Dict[str, Any] = {
+ "metadata": dist.metadata_dict,
+ "metadata_location": dist.info_location,
+ }
+ # direct_url. Note that we don't have download_info (as in the installation
+ # report) since it is not recorded in installed metadata.
+ direct_url = dist.direct_url
+ if direct_url is not None:
+ res["direct_url"] = direct_url.to_dict()
+ else:
+ # Emulate direct_url for legacy editable installs.
+ editable_project_location = dist.editable_project_location
+ if editable_project_location is not None:
+ res["direct_url"] = {
+ "url": path_to_url(editable_project_location),
+ "dir_info": {
+ "editable": True,
+ },
+ }
+ # installer
+ installer = dist.installer
+ if dist.installer:
+ res["installer"] = installer
+ # requested
+ if dist.installed_with_dist_info:
+ res["requested"] = dist.requested
+ return res