Skip to content

Commit e58d641

Browse files
authored
Drop support for Python 3.9 (#916)
1 parent 30aef9d commit e58d641

35 files changed

Lines changed: 318 additions & 385 deletions

.github/workflows/manual.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ jobs:
2020
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
2121
with:
2222
python-version: |
23-
3.9
2423
3.10
2524
3.11
2625
3.12

.github/workflows/tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
22+
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
2323
steps:
2424
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2525
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -267,7 +267,7 @@ jobs:
267267
-Dsonar.exclusions=sphinx/**
268268
-Dsonar.tests=jsonargparse_tests
269269
-Dsonar.python.coverage.reportPaths=coverage_*.xml
270-
-Dsonar.python.version=3.9,3.10,3.11,3.12,3.13,3.14
270+
-Dsonar.python.version=3.10,3.11,3.12,3.13,3.14
271271
env:
272272
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
273273
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ The semantic versioning only considers the public API as described in
1212
paths are considered internals and can change in minor and patch releases.
1313

1414

15+
v4.50.0 (unreleased)
16+
--------------------
17+
18+
Changed
19+
^^^^^^^
20+
- Drop support for Python 3.9. The minimum supported Python version is now
21+
3.10 (`#916 <https://github.com/omni-us/jsonargparse/pull/916>`__).
22+
23+
1524
v4.49.0 (2026-05-15)
1625
--------------------
1726

DOCUMENTATION.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,11 @@ Some notes about this support are:
443443
limit in nesting depth.
444444

445445
- Postponed evaluation of types PEP `563 <https://peps.python.org/pep-0563/>`__
446-
(i.e. ``from __future__ import annotations``) is supported. Also supported on
447-
``python==3.9`` is PEP `604 <https://peps.python.org/pep-0604/>`__ (i.e.
448-
``<type> | <type>`` instead of ``Union[<type>, <type>]``).
446+
(i.e. ``from __future__ import annotations``) is supported. Also supported are
447+
PEP `585 <https://peps.python.org/pep-0585/>`__ (i.e. ``list[<type>],
448+
dict[<type>], ...`` instead of ``List[<type>], Dict[<type>], ...``) and `604
449+
<https://peps.python.org/pep-0604/>`__ (i.e. ``<type> | <type>`` instead of
450+
``Union[<type>, <type>]``).
449451

450452
- Types that use components imported inside ``TYPE_CHECKING`` blocks are
451453
supported.
@@ -1875,9 +1877,7 @@ jsonargparse with the ``signatures`` extra as explained in section
18751877

18761878
Many of the types defined in stub files use the latest syntax for type hints,
18771879
that is, bitwise or operator ``|`` for unions, see PEP `604
1878-
<https://peps.python.org/pep-0604>`__. On ``python>=3.10`` these are fully
1879-
supported. On ``python<=3.9`` backporting these types is attempted and in some
1880-
cases it can fail. On failure the type annotation is set to ``Any``.
1880+
<https://peps.python.org/pep-0604>`__. This syntax is fully supported.
18811881

18821882
Most of the types in the Python standard library have their types in stubs. An
18831883
example from the standard library would be:

jsonargparse/_actions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from argparse import Action as ArgparseAction
77
from contextlib import contextmanager
88
from contextvars import ContextVar
9-
from typing import Any, Optional
9+
from typing import Any
1010

1111
from ._common import Action, NonParsingAction, is_subclass, is_subclasses_disabled, parser_context
1212
from ._loaders_dumpers import get_loader_exceptions, load_value
@@ -116,7 +116,7 @@ def apply_config(parser, cfg, dest, value) -> None:
116116
with parser_context(single_subcommand=False), previous_config_context(cfg), skip_apply_links():
117117
kwargs = {"env": False, "defaults": False, "_skip_validation": True, "_fail_no_subcommand": False}
118118
try:
119-
cfg_path: Optional[Path] = Path(value, mode=_get_config_read_mode())
119+
cfg_path: Path | None = Path(value, mode=_get_config_read_mode())
120120
except TypeError as ex_path:
121121
try:
122122
if isinstance(load_value(value), str):
@@ -224,7 +224,7 @@ def is_print_config_requested(parser):
224224

225225

226226
class _ActionConfigLoad(Action):
227-
def __init__(self, basetype: Optional[type] = None, **kwargs):
227+
def __init__(self, basetype: type | None = None, **kwargs):
228228
if len(kwargs) == 0:
229229
self._basetype = basetype
230230
else:
@@ -267,7 +267,7 @@ class _ActionHelpClassPath(NonParsingAction):
267267
sub_add_kwargs: dict[str, Any] = {}
268268

269269
@classmethod
270-
def get_help_types(cls, typehint) -> Optional[tuple]:
270+
def get_help_types(cls, typehint) -> tuple | None:
271271
from ._typehints import get_subclass_or_closed_types
272272

273273
return get_subclass_or_closed_types(typehint=typehint, also_lists=True, callable_return=True)

jsonargparse/_cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Simple creation of command line interfaces."""
22

33
import inspect
4-
from typing import Any, Callable, Optional, Union
4+
from collections.abc import Callable
5+
from typing import Any, Optional, Union
56

67
from ._actions import ActionConfigFile, _ActionPrintConfig, remove_actions
78
from ._core import ArgumentParser
@@ -25,9 +26,9 @@ def CLI(*args, **kwargs):
2526

2627
def auto_cli(
2728
components: ComponentsType = None,
28-
args: Optional[list[str]] = None,
29+
args: list[str] | None = None,
2930
config_help: str = default_config_option_help,
30-
set_defaults: Optional[dict[str, Any]] = None,
31+
set_defaults: dict[str, Any] | None = None,
3132
as_positional: bool = True,
3233
return_instance: bool = False,
3334
fail_untyped: bool = True,

jsonargparse/_common.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
import inspect
44
import logging
55
import os
6+
from collections.abc import Callable
67
from contextlib import contextmanager
78
from contextvars import ContextVar
89
from typing import ( # type: ignore[attr-defined]
9-
Callable,
1010
Generic,
1111
Optional,
1212
Protocol,
1313
TypeVar,
14-
Union,
1514
_GenericAlias,
1615
)
1716

@@ -52,17 +51,17 @@ def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType:
5251
InstantiatorsDictType = dict[tuple[type, bool], InstantiatorCallable]
5352

5453

55-
parent_parser: ContextVar[Optional[ArgumentParser]] = ContextVar("parent_parser", default=None)
54+
parent_parser: ContextVar[ArgumentParser | None] = ContextVar("parent_parser", default=None)
5655
parser_capture: ContextVar[bool] = ContextVar("parser_capture", default=False)
57-
defaults_cache: ContextVar[Optional[Namespace]] = ContextVar("defaults_cache", default=None)
58-
lenient_check: ContextVar[Union[bool, str]] = ContextVar("lenient_check", default=False)
56+
defaults_cache: ContextVar[Namespace | None] = ContextVar("defaults_cache", default=None)
57+
lenient_check: ContextVar[bool | str] = ContextVar("lenient_check", default=False)
5958
parsing_defaults: ContextVar[bool] = ContextVar("parsing_defaults", default=False)
6059
single_subcommand: ContextVar[bool] = ContextVar("single_subcommand", default=True)
6160
validating_defaults: ContextVar[bool] = ContextVar("validating_defaults", default=False)
62-
load_value_mode: ContextVar[Optional[str]] = ContextVar("load_value_mode", default=None)
63-
class_instantiators: ContextVar[Optional[InstantiatorsDictType]] = ContextVar("class_instantiators", default=None)
61+
load_value_mode: ContextVar[str | None] = ContextVar("load_value_mode", default=None)
62+
class_instantiators: ContextVar[InstantiatorsDictType | None] = ContextVar("class_instantiators", default=None)
6463
nested_links: ContextVar[list[dict]] = ContextVar("nested_links", default=[])
65-
applied_instantiation_links: ContextVar[Optional[set]] = ContextVar("applied_instantiation_links", default=None)
64+
applied_instantiation_links: ContextVar[set | None] = ContextVar("applied_instantiation_links", default=None)
6665
path_dump_preserve_relative: ContextVar[bool] = ContextVar("path_dump_preserve_relative", default=False)
6766

6867

@@ -115,17 +114,17 @@ def get_env_var_bool(name: str) -> bool:
115114

116115
def set_parsing_settings(
117116
*,
118-
validate_defaults: Optional[bool] = None,
119-
config_read_mode_urls_enabled: Optional[bool] = None,
120-
config_read_mode_fsspec_enabled: Optional[bool] = None,
117+
validate_defaults: bool | None = None,
118+
config_read_mode_urls_enabled: bool | None = None,
119+
config_read_mode_fsspec_enabled: bool | None = None,
121120
docstring_parse_style: Optional["docstring_parser.DocstringStyle"] = None,
122-
docstring_parse_attribute_docstrings: Optional[bool] = None,
123-
parse_optionals_as_positionals: Optional[bool] = None,
124-
add_print_completion_argument: Optional[bool] = None,
125-
stubs_resolver_allow_py_files: Optional[bool] = None,
126-
omegaconf_absolute_to_relative_paths: Optional[bool] = None,
127-
subclasses_disabled: Optional[list[Union[type, Callable[[type], bool]]]] = None,
128-
subclasses_enabled: Optional[list[Union[type, str]]] = None,
121+
docstring_parse_attribute_docstrings: bool | None = None,
122+
parse_optionals_as_positionals: bool | None = None,
123+
add_print_completion_argument: bool | None = None,
124+
stubs_resolver_allow_py_files: bool | None = None,
125+
omegaconf_absolute_to_relative_paths: bool | None = None,
126+
subclasses_disabled: list[type | Callable[[type], bool]] | None = None,
127+
subclasses_enabled: list[type | str] | None = None,
129128
) -> None:
130129
"""
131130
Modify global parser settings that affect parser creation and parsing behavior.
@@ -329,7 +328,7 @@ def is_pure_dataclass(cls) -> bool:
329328

330329
subclasses_enabled_types: set[type] = set()
331330
subclasses_disabled_types: set[type] = set()
332-
subclasses_disabled_selectors: dict[str, Callable[[type], Union[bool, int]]] = {
331+
subclasses_disabled_selectors: dict[str, Callable[[type], bool | int]] = {
333332
"is_pure_dataclass": is_pure_dataclass,
334333
"is_pydantic_model": is_pydantic_model,
335334
"is_attrs_class": is_attrs_class,
@@ -351,8 +350,8 @@ def is_subclasses_disabled(cls) -> bool:
351350

352351

353352
def subclass_type_behavior(
354-
subclasses_disabled: Optional[list[Union[type, Callable[[type], bool]]]] = None,
355-
subclasses_enabled: Optional[list[Union[type, str]]] = None,
353+
subclasses_disabled: list[type | Callable[[type], bool]] | None = None,
354+
subclasses_enabled: list[type | str] | None = None,
356355
) -> None:
357356
"""Configures whether class types accept or not subclasses."""
358357
for enable_item in subclasses_enabled or []:
@@ -404,7 +403,7 @@ def setup_default_logger(data, level, caller):
404403
return logger
405404

406405

407-
def parse_logger(logger: Union[bool, str, dict, logging.Logger], caller):
406+
def parse_logger(logger: bool | str | dict | logging.Logger, caller):
408407
if not isinstance(logger, (bool, str, dict, logging.Logger)):
409408
raise ValueError(f"Expected logger to be an instance of (bool, str, dict, logging.Logger), but got {logger}.")
410409
if isinstance(logger, dict) and len(set(logger) - {"name", "level"}) > 0:
@@ -425,7 +424,7 @@ def parse_logger(logger: Union[bool, str, dict, logging.Logger], caller):
425424
class LoggerProperty:
426425
"""Class designed to be inherited by other classes to add a logger property."""
427426

428-
def __init__(self, *args, logger: Union[bool, str, dict, logging.Logger] = False, **kwargs):
427+
def __init__(self, *args, logger: bool | str | dict | logging.Logger = False, **kwargs):
429428
self.logger = logger
430429
super().__init__(*args, **kwargs)
431430

@@ -444,7 +443,7 @@ def logger(self) -> logging.Logger:
444443
return self._logger
445444

446445
@logger.setter
447-
def logger(self, logger: Union[bool, str, dict, logging.Logger]):
446+
def logger(self, logger: bool | str | dict | logging.Logger):
448447
if logger is None:
449448
from ._deprecated import deprecation_warning, logger_property_none_message
450449

jsonargparse/_completions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from enum import Enum
1111
from importlib.util import find_spec
1212
from subprocess import PIPE, Popen
13-
from typing import Literal, Optional, Union
13+
from typing import Literal, Union
1414

1515
from ._actions import ActionConfigFile, ActionFail, _ActionConfigLoad, _ActionHelpClassPath, remove_actions
1616
from ._common import NonParsingAction, get_optionals_as_positionals_actions, get_parsing_setting
@@ -141,7 +141,7 @@ def get_completion_script(parser, completion_type: str, **kwargs) -> str:
141141
return get_shtab_script(parser, completion_type[len("shtab-") :], **kwargs)
142142

143143

144-
def get_shtab_script(parser, shell: str, preambles: Optional[list[str]] = None) -> str:
144+
def get_shtab_script(parser, shell: str, preambles: list[str] | None = None) -> str:
145145
import shtab
146146

147147
if shell not in shtab.SUPPORTED_SHELLS:

0 commit comments

Comments
 (0)