1from __future__ import annotations
2
3import importlib
4import sys
5from typing import (
6 TYPE_CHECKING,
7 Literal,
8 overload,
9)
10import warnings
11
12from pandas.util._exceptions import find_stack_level
13
14from pandas.util.version import Version
15
16if TYPE_CHECKING:
17 import types
18
19# Update install.rst, pixi.toml,
20# deps_minimum.toml & pyproject.toml when updating versions!
21
22VERSIONS = {
23 "adbc-driver-postgresql": "1.7.0",
24 "adbc-driver-sqlite": "1.7.0",
25 "bs4": "4.13.4",
26 "bottleneck": "1.5.0",
27 "fastparquet": "2024.11.0",
28 "fsspec": "2025.7.0",
29 "html5lib": "1.1",
30 "gcsfs": "2025.7.0",
31 "jinja2": "3.1.6",
32 "lxml.etree": "6.0.0",
33 "matplotlib": "3.10.5",
34 "numba": "0.61.2",
35 "numexpr": "2.11.0",
36 "odfpy": "1.4.1",
37 "openpyxl": "3.1.5",
38 "psycopg2": "2.9.10", # (dt dec pq3 ext lo64)
39 "pymysql": "1.1.1",
40 "pyarrow": "16.0.0",
41 "pyiceberg": "0.9.1",
42 "pyreadstat": "1.3.0",
43 "pytest": "8.3.4",
44 "python-calamine": "0.4.0",
45 "pytz": "2020.1", # keep this pinned (https://github.com/pandas-dev/pandas/pull/65133)
46 "pyxlsb": "1.0.10",
47 "s3fs": "2025.7.0",
48 "scipy": "1.16.1",
49 "sqlalchemy": "2.0.42",
50 "tables": "3.10.2",
51 "tabulate": "0.9.0",
52 "xarray": "2025.7.1",
53 "xlrd": "2.0.2",
54 "xlsxwriter": "3.2.5",
55 "zstandard": "0.23.0",
56 "qtpy": "2.4.3",
57 "pyqt5": "5.15.11",
58}
59
60# A mapping from import name to package name (on PyPI) for packages where
61# these two names are different.
62
63INSTALL_MAPPING = {
64 "bs4": "beautifulsoup4",
65 "bottleneck": "Bottleneck",
66 "jinja2": "Jinja2",
67 "lxml.etree": "lxml",
68 "odf": "odfpy",
69 "python_calamine": "python-calamine",
70 "sqlalchemy": "SQLAlchemy",
71 "tables": "pytables",
72}
73
74
75def get_version(module: types.ModuleType) -> str:
76 version = getattr(module, "__version__", None)
77
78 if version is None:
79 raise ImportError(f"Can't determine version for {module.__name__}")
80 if module.__name__ == "psycopg2":
81 # psycopg2 appends " (dt dec pq3 ext lo64)" to its version
82 version = version.split()[0]
83 return version
84
85
86@overload
87def import_optional_dependency(
88 name: str,
89 extra: str = ...,
90 min_version: str | None = ...,
91 *,
92 errors: Literal["raise"] = ...,
93) -> types.ModuleType: ...
94
95
96@overload
97def import_optional_dependency(
98 name: str,
99 extra: str = ...,
100 min_version: str | None = ...,
101 *,
102 errors: Literal["warn", "ignore"],
103) -> types.ModuleType | None: ...
104
105
106def import_optional_dependency(
107 name: str,
108 extra: str = "",
109 min_version: str | None = None,
110 *,
111 errors: Literal["raise", "warn", "ignore"] = "raise",
112) -> types.ModuleType | None:
113 """
114 Import an optional dependency.
115
116 By default, if a dependency is missing an ImportError with a nice
117 message will be raised. If a dependency is present, but too old,
118 we raise.
119
120 Parameters
121 ----------
122 name : str
123 The module name.
124 extra : str
125 Additional text to include in the ImportError message.
126 errors : str {'raise', 'warn', 'ignore'}
127 What to do when a dependency is not found or its version is too old.
128
129 * raise : Raise an ImportError
130 * warn : Only applicable when a module's version is to old.
131 Warns that the version is too old and returns None
132 * ignore: If the module is not installed, return None, otherwise,
133 return the module, even if the version is too old.
134 It's expected that users validate the version locally when
135 using ``errors="ignore"`` (see. ``io/html.py``)
136 min_version : str, default None
137 Specify a minimum version that is different from the global pandas
138 minimum version required.
139 Returns
140 -------
141 maybe_module : Optional[ModuleType]
142 The imported module, when found and the version is correct.
143 None is returned when the package is not found and `errors`
144 is False, or when the package's version is too old and `errors`
145 is ``'warn'`` or ``'ignore'``.
146 """
147 assert errors in {"warn", "raise", "ignore"}
148
149 package_name = INSTALL_MAPPING.get(name)
150 install_name = package_name if package_name is not None else name
151
152 msg = (
153 f"`Import {install_name}` failed. {extra} "
154 f"Use pip, conda, or your preferred package management tool "
155 f"to install the {install_name} package."
156 )
157 try:
158 module = importlib.import_module(name)
159 except ImportError as err:
160 if errors == "raise":
161 raise ImportError(msg) from err
162 return None
163
164 # Handle submodules: if we have submodule, grab parent module from sys.modules
165 parent = name.split(".", maxsplit=1)[0]
166 if parent != name:
167 install_name = parent
168 module_to_get = sys.modules[install_name]
169 else:
170 module_to_get = module
171 minimum_version = min_version if min_version is not None else VERSIONS.get(parent)
172 if minimum_version:
173 version = get_version(module_to_get)
174 if version and Version(version) < Version(minimum_version):
175 msg = (
176 f"Pandas requires version '{minimum_version}' or newer of '{parent}' "
177 f"(version '{version}' currently installed)."
178 )
179 if errors == "warn":
180 warnings.warn(
181 msg,
182 UserWarning,
183 stacklevel=find_stack_level(),
184 )
185 return None
186 elif errors == "raise":
187 raise ImportError(msg)
188 else:
189 return None
190
191 return module