diff --git a/rbinstall/pypi.py b/rbinstall/pypi.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3c0c98960f65b3a534985468db37da9c60167fe
--- /dev/null
+++ b/rbinstall/pypi.py
@@ -0,0 +1,168 @@
+"""PyPI lookup support.
+
+Version Added:
+    1.0
+"""
+
+from __future__ import annotations
+
+import json
+from typing import Optional, TYPE_CHECKING
+from urllib.error import HTTPError
+from urllib.parse import urljoin
+from urllib.request import Request, urlopen
+
+from packaging.specifiers import SpecifierSet
+from packaging.version import parse as parse_version
+from typing_extensions import TypedDict
+
+from rbinstall import get_package_version
+from rbinstall.errors import InstallerError
+
+if TYPE_CHECKING:
+    from rbinstall.state import SystemInfo
+
+
+USER_AGENT = f'rbinstall/{get_package_version()}'
+
+
+class PackageVersionInfo(TypedDict):
+    """Information on a Python package to install.
+
+    Version Added:
+        1.0
+    """
+
+    #: Whether this is the latest stable version of the package.
+    is_latest: bool
+
+    #: Whether this is the requested version of the package.
+    #:
+    #: This will be False if an older version had to be returned for
+    #: compatibility purposes.
+    is_requested: bool
+
+    #: The latest version of the package.
+    latest_version: str
+
+    #: The name of the package to install.
+    package_name: str
+
+    #: The required Python version specifier.
+    requires_python: str
+
+    #: The version of the package.
+    version: str
+
+
+def get_package_version_info(
+    *,
+    system_info: SystemInfo,
+    package_name: str,
+    target_version: str = 'latest',
+    pypi_url: str = 'https://pypi.org',
+) -> Optional[PackageVersionInfo]:
+    """Return information on a version of a package.
+
+    This will return some basic information that can be used to verify that
+    the version of Review Board is available and can be installed on the
+    target system.
+
+    Args:
+        system_info (rbinstall.state.SystemInfo):
+            Information on the target system.
+
+        package_name (str):
+            The name of the package.
+
+        target_version (str, optional):
+            The target version of Review Board requested.
+
+            An older version may be returned, if the target version is not
+            compatible with the current system.
+
+        pypi_url (str, optional):
+            The optional URL to the PyPI server.
+
+    Returns:
+        PackageVersionInfo:
+        Information on the nearest compatible version of Review Board.
+    """
+    url = urljoin(pypi_url, f'pypi/{package_name}/json')
+
+    request = Request(url)
+    request.add_header('Accept', 'application/json')
+    request.add_header('User-Agent', USER_AGENT)
+
+    try:
+        try:
+            with urlopen(request) as fp:
+                rsp = json.load(fp)
+        except HTTPError as e:
+            if e.code == 404:
+                return None
+
+            raise
+    except Exception as e:
+        raise InstallerError(
+            f'Could not fetch information on the {package_name} packages '
+            f'(at {url}). Check your network and HTTP(S) proxy environment '
+            f'variables (`http_proxy` and `https_proxy`). The error was: '
+            f'{e}'
+        )
+
+    python_version = '%s.%s.%s' % system_info['system_python_version'][:3]
+
+    try:
+        rsp_info = rsp['info']
+        latest_version = rsp_info['version']
+        parsed_latest_version = parse_version(latest_version)
+
+        if target_version == 'latest':
+            target_version = latest_version
+
+        parsed_target_version = parse_version(target_version)
+
+        # Find the nearest compatible version of Review Board.
+        rsp_releases = sorted(
+            rsp['releases'].items(),
+            key=lambda pair: parse_version(pair[0]),
+            reverse=True)
+
+        for rsp_version, rsp_release in rsp_releases:
+            if not rsp_release:
+                continue
+
+            parsed_rsp_version = parse_version(rsp_version)
+
+            if parsed_rsp_version > parsed_target_version:
+                continue
+
+            rsp_dist = rsp_release[0]
+
+            if rsp_dist.get('yanked'):
+                continue
+
+            requires_python = rsp_dist.get('requires_python', '')
+
+            if (not requires_python or
+                python_version in SpecifierSet(requires_python)):
+                # This is a compatible version. Return it.
+                return {
+                    'is_latest': parsed_rsp_version == parsed_latest_version,
+                    'is_requested':
+                        parsed_rsp_version == parsed_target_version,
+                    'latest_version': latest_version,
+                    'package_name': rsp_info['name'],
+                    'requires_python': requires_python,
+                    'version': rsp_version,
+                }
+
+        return None
+    except Exception as e:
+        raise InstallerError(
+            f'Could not parse information on {package_name} packages '
+            f'(at {url}). This may indicate an issue accessing '
+            f'https://pypi.org/ or an issue with the requested version of '
+            f'Review Board. The error was: {e}'
+        )
diff --git a/rbinstall/tests/test_pypi.py b/rbinstall/tests/test_pypi.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef53575f531da13bffccc9ab8a1f7bcdc6c5fb96
--- /dev/null
+++ b/rbinstall/tests/test_pypi.py
@@ -0,0 +1,274 @@
+"""Unit tests for rbinstall.pypi.
+
+Version Added:
+    1.0
+"""
+
+from __future__ import annotations
+
+import json
+import re
+from contextlib import contextmanager
+from io import StringIO
+from typing import Any, Dict, TYPE_CHECKING, Tuple
+from unittest import TestCase
+from urllib.error import HTTPError
+from urllib.request import urlopen
+
+import kgb
+
+from rbinstall.errors import InstallerError
+from rbinstall.install_methods import InstallMethodType
+from rbinstall.pypi import get_package_version_info
+
+if TYPE_CHECKING:
+    from rbinstall.state import SystemInfo
+
+
+class GetPackageVersionInfoTests(kgb.SpyAgency, TestCase):
+    """Unit tests for get_package_version_info().
+
+    Version Added:
+        1.0
+    """
+
+    def test_with_latest_match(self) -> None:
+        """Testing get_package_version_info with latest version match"""
+        self._setup_response({
+            'info': {
+                'name': 'ReviewBoard',
+                'version': '6.0.1',
+            },
+            'releases': {
+                '4.0': [],
+                '6.0.1': [
+                    {
+                        'requires_python': '>=3.8',
+                    },
+                ],
+            },
+        })
+
+        info = get_package_version_info(
+            system_info=self.create_system_info(),
+            package_name='ReviewBoard',
+            target_version='latest')
+
+        self.assertEqual(
+            info,
+            {
+                'is_latest': True,
+                'is_requested': True,
+                'latest_version': '6.0.1',
+                'package_name': 'ReviewBoard',
+                'requires_python': '>=3.8',
+                'version': '6.0.1',
+            })
+
+    def test_with_specific_latest_match(self) -> None:
+        """Testing get_package_version_info with specific latest version match
+        """
+        self._setup_response({
+            'info': {
+                'name': 'ReviewBoard',
+                'version': '6.0.1',
+            },
+            'releases': {
+                '4.0': [],
+                '6.0.1': [
+                    {
+                        'requires_python': '>=3.8',
+                    },
+                ],
+            },
+        })
+
+        info = get_package_version_info(
+            system_info=self.create_system_info(),
+            package_name='ReviewBoard',
+            target_version='6.0.1')
+
+        self.assertEqual(
+            info,
+            {
+                'is_latest': True,
+                'is_requested': True,
+                'latest_version': '6.0.1',
+                'package_name': 'ReviewBoard',
+                'requires_python': '>=3.8',
+                'version': '6.0.1',
+            })
+
+    def test_with_below_latest_match(self) -> None:
+        """Testing get_package_version_info with below latest version match"""
+        self._setup_response({
+            'info': {
+                'name': 'ReviewBoard',
+                'version': '6.0.1',
+            },
+            'releases': {
+                '4.0': [],
+                '5.0': [
+                    {
+                        'requires_python': '>=3.7',
+                    },
+                ],
+                '6.0.1': [
+                    {
+                        'requires_python': '>=3.8',
+                    },
+                ],
+            },
+        })
+
+        info = get_package_version_info(
+            system_info=self.create_system_info(python_version=(3, 7, 0)),
+            package_name='ReviewBoard',
+            target_version='latest')
+
+        self.assertEqual(
+            info,
+            {
+                'is_latest': False,
+                'is_requested': False,
+                'latest_version': '6.0.1',
+                'package_name': 'ReviewBoard',
+                'requires_python': '>=3.7',
+                'version': '5.0',
+            })
+
+    def test_with_no_match(self) -> None:
+        """Testing get_package_version_info with no match"""
+        self._setup_response({
+            'info': {
+                'name': 'ReviewBoard',
+                'version': '6.0.1',
+            },
+            'releases': {
+                '5.0': [
+                    {
+                        'requires_python': '>=3.7',
+                    },
+                ],
+                '6.0.1': [
+                    {
+                        'requires_python': '>=3.8',
+                    },
+                ],
+            },
+        })
+
+        info = get_package_version_info(
+            system_info=self.create_system_info(python_version=(2, 7, 0)),
+            package_name='ReviewBoard',
+            target_version='latest')
+
+        self.assertIsNone(info)
+
+    def test_with_http_error(self) -> None:
+        """Testing get_package_version_info with HTTP error"""
+        self.spy_on(
+            urlopen,
+            op=kgb.SpyOpRaise(HTTPError(
+                url='https://pypi.org/pypi/ReviewBoard/json',
+                code=500,
+                msg='Internal Server Error',
+                hdrs={},  # type: ignore
+                fp=None)))
+
+        message = re.escape(
+            'Could not fetch information on the ReviewBoard packages (at '
+            'https://pypi.org/pypi/ReviewBoard/json). Check your network '
+            'and HTTP(S) proxy environment variables (`http_proxy` and '
+            '`https_proxy`). The error was: HTTP Error 500: Internal Server '
+            'Error'
+        )
+
+        with self.assertRaisesRegex(InstallerError, message):
+            get_package_version_info(
+                system_info=self.create_system_info(python_version=(2, 7, 0)),
+                package_name='ReviewBoard',
+                target_version='latest')
+
+    def test_with_http_error_404(self) -> None:
+        """Testing get_package_version_info with HTTP error 404"""
+        self.spy_on(
+            urlopen,
+            op=kgb.SpyOpRaise(HTTPError(
+                url='https://pypi.org/pypi/ReviewBoard/json',
+                code=404,
+                msg='Not Found',
+                hdrs={},  # type: ignore
+                fp=None)))
+
+        info = get_package_version_info(
+            system_info=self.create_system_info(),
+            package_name='ReviewBoard',
+            target_version='latest')
+
+        self.assertIsNone(info)
+
+    def test_with_parse_error(self) -> None:
+        """Testing get_package_version_info with parse error"""
+        self._setup_response({})
+
+        message = re.escape(
+            "Could not parse information on ReviewBoard packages (at "
+            "https://pypi.org/pypi/ReviewBoard/json). This may indicate an "
+            "issue accessing https://pypi.org/ or an issue with the requested "
+            "version of Review Board. The error was: 'info'"
+        )
+
+        with self.assertRaisesRegex(InstallerError, message):
+            get_package_version_info(
+                system_info=self.create_system_info(),
+                package_name='ReviewBoard',
+                target_version='latest')
+
+    def create_system_info(
+        self,
+        *,
+        python_version: Tuple[int, int, int] = (3, 11, 0),
+    ) -> SystemInfo:
+        """Return sample system information for testing.
+
+        Args:
+            python_version (tuple of int):
+                A 3-tuple for the Python version to test with.
+
+        Returns:
+            rbinstall.state.SystemInfo:
+            The generated system information.
+        """
+        return {
+            'arch': 'amd64',
+            'bootstrap_python_exe': '/path/to/bootstrap/python',
+            'paths': {},
+            'system_install_method': InstallMethodType.APT,
+            'system': 'Linux',
+            'system_python_exe': '/usr/bin/python',
+            'system_python_version': (*python_version, '', 0),
+            'version': '1.2.3',
+        }
+
+    def _setup_response(
+        self,
+        rsp: Dict[str, Any],
+    ) -> None:
+        """Set up an HTTP response for a test.
+
+        Args:
+            rsp (dict):
+                The payload to return in the response.
+        """
+        @self.spy_for(urlopen)
+        @contextmanager
+        def _urlopen(request, *args, **kwargs):
+            headers = request.headers
+
+            self.assertEqual(request.get_full_url(),
+                             'https://pypi.org/pypi/ReviewBoard/json')
+            self.assertEqual(headers['Accept'], 'application/json')
+            self.assertTrue(headers['User-agent'].startswith('rbinstall/'))
+
+            yield StringIO(json.dumps(rsp))
