Repository: optuna/optuna

Issue:
Use `__future__.annotations` everywhere in the Optuna code base
### Motivation

Optuna drops Python 3.6 from v3.1, so we can use `__future__.annotations`, which simplifies the code base. See [PEP 563](https://peps.python.org/pep-0563/), [PEP584](https://peps.python.org/pep-0584/), [PEP 585](https://peps.python.org/pep-0585/), and [PEP 604](https://peps.python.org/pep-0604/) for more details. This issue suggests to use the module and simplifies the code base.

### Suggestion

Use `__future__.annotations` for each file and simplify the type annotations. The list of classes whose type annotations can be simplified is [here](https://peps.python.org/pep-0585/#implementation). The list of files where the `__future__.annotations` can be used is as follows. In order to reduce review costs and to encourage more contributors to work on it, please, as a rule, fix one file per PR.

- [x] optuna/_convert_positional_args.py
- [x] optuna/visualization/_optimization_history.py
- [x] optuna/visualization/_hypervolume_history.py
- [x] optuna/visualization/_edf.py
- [x] optuna/visualization/_pareto_front.py
- [x] optuna/visualization/matplotlib/_optimization_history.py
- [x] optuna/visualization/matplotlib/_hypervolume_history.py
- [x] optuna/visualization/matplotlib/_edf.py
- [x] optuna/visualization/matplotlib/_pareto_front.py
- [x] optuna/visualization/matplotlib/_contour.py
- [x] optuna/visualization/_utils.py
- [x] optuna/logging.py
- [ ] optuna/storages/_base.py
- [ ] optuna/storages/_cached_storage.py
- [ ] optuna/storages/__init__.py
- [ ] optuna/storages/_heartbeat.py
- [ ] optuna/storages/_in_memory.py
- [ ] optuna/storages/_rdb/models.py
- [ ] optuna/storages/_rdb/storage.py
- [ ] optuna/storages/_rdb/alembic/versions/v3.0.0.c.py
- [ ] optuna/storages/_rdb/alembic/versions/v3.0.0.d.py
- [ ] optuna/storages/_rdb/alembic/versions/v3.0.0.a.py
- [ ] optuna/storages/_journal/file.py
- [ ] optuna/storages/_journal/redis.py
- [ ] optuna/storages/_journal/storage.py
- [ ] optuna/storages/_journal/base.py
- [ ] optuna/study/_dataframe.py
- [ ] optuna/study/_optimize.py
- [ ] optuna/study/_tell.py
- [ ] optuna/study/_multi_objective.py
- [ ] optuna/study/_frozen.py
- [ ] optuna/study/study.py
- [ ] optuna/study/_study_summary.py
- [ ] optuna/search_space/group_decomposed.py
- [ ] optuna/search_space/intersection.py
- [ ] optuna/_typing.py
- [ ] optuna/_deprecated.py
- [ ] optuna/pruners/_hyperband.py
- [ ] optuna/pruners/_patient.py
- [ ] optuna/pruners/_successive_halving.py
- [ ] optuna/pruners/_percentile.py
- [ ] optuna/pruners/_threshold.py
- [ ] optuna/trial/_base.py
- [ ] optuna/trial/_fixed.py
- [ ] optuna/trial/_trial.py
- [ ] optuna/trial/_frozen.py
- [ ] optuna/integration/cma.py
- [ ] optuna/integration/shap.py
- [ ] optuna/integration/lightgbm.py
- [ ] optuna/integration/pytorch_distributed.py
- [ ] optuna/integration/_lightgbm_tuner/optimize.py
- [ ] optuna/integration/_lightgbm_tuner/alias.py
- [ ] optuna/integration/mlflow.py
- [ ] optuna/integration/wandb.py
- [ ] optuna/integration/catboost.py
- [ ] optuna/integration/skopt.py
- [ ] optuna/integration/botorch.py
- [ ] optuna/integration/dask.py
- [x] optuna/integration/sklearn.py
- [ ] optuna/integration/tensorboard.py
- [ ] optuna/terminator/callback.py
- [ ] optuna/terminator/terminator.py
- [ ] optuna/terminator/improvement/_preprocessing.py
- [ ] optuna/terminator/improvement/gp/botorch.py
- [ ] optuna/terminator/improvement/gp/base.py
- [ ] optuna/terminator/improvement/evaluator.py
- [ ] optuna/importance/_base.py
- [ ] optuna/importance/_mean_decrease_impurity.py
- [ ] optuna/importance/__init__.py
- [ ] optuna/importance/_fanova/_fanova.py
- [ ] optuna/importance/_fanova/_evaluator.py
- [ ] optuna/importance/_fanova/_tree.py
- [ ] optuna/_imports.py
- [ ] optuna/testing/tempfile_pool.py
- [ ] optuna/testing/threading.py
- [ ] optuna/testing/distributions.py
- [ ] optuna/testing/samplers.py
- [ ] optuna/testing/storages.py
- [ ] optuna/distributions.py
- [ ] optuna/cli.py
- [ ] optuna/multi_objective/visualization/_pareto_front.py
- [ ] optuna/multi_objective/trial.py
- [ ] optuna/multi_objective/samplers/_base.py
- [ ] optuna/multi_objective/samplers/_nsga2.py
- [ ] optuna/multi_objective/samplers/_adapter.py
- [ ] optuna/multi_objective/samplers/_random.py
- [ ] optuna/multi_objective/samplers/_motpe.py
- [ ] optuna/multi_objective/study.py
- [ ] optuna/_experimental.py
- [ ] optuna/samplers/_base.py
- [ ] optuna/samplers/nsgaii/_crossovers/_undx.py
- [ ] optuna/samplers/nsgaii/_crossovers/_spx.py
- [ ] optuna/samplers/nsgaii/_crossovers/_sbx.py
- [ ] optuna/samplers/nsgaii/_crossovers/_vsbx.py
- [ ] optuna/samplers/nsgaii/_sampler.py
- [ ] optuna/samplers/nsgaii/_crossover.py
- [ ] optuna/samplers/_search_space/intersection.py
- [ ] optuna/samplers/_qmc.py
- [ ] optuna/samplers/_tpe/probability_distributions.py
- [ ] optuna/samplers/_tpe/_truncnorm.py
- [ ] optuna/samplers/_tpe/multi_objective_sampler.py
- [ ] optuna/samplers/_tpe/parzen_estimator.py
- [ ] optuna/samplers/_tpe/sampler.py
- [ ] optuna/samplers/_random.py
- [ ] optuna/samplers/_cmaes.py
- [ ] optuna/samplers/_partial_fixed.py
- [ ] optuna/samplers/_brute_force.py
- [ ] optuna/samplers/_nsgaiii.py
- [ ] optuna/samplers/_grid.py
- [ ] optuna/_hypervolume/wfg.py
- [ ] optuna/_hypervolume/hssp.py
- [ ] optuna/progress_bar.py
- [ ] optuna/_transform.py
- [ ] optuna/_callbacks.py
- [ ] tests/multi_objective_tests/test_study.py
- [ ] tests/multi_objective_tests/samplers_tests/test_motpe.py
- [ ] tests/multi_objective_tests/samplers_tests/test_nsga2.py
- [ ] tests/multi_objective_tests/test_trial.py
- [ ] tests/multi_objective_tests/visualization_tests/test_pareto_front.py
- [ ] tests/trial_tests/test_frozen.py
- [ ] tests/trial_tests/test_trials.py
- [ ] tests/trial_tests/test_trial.py
- [ ] tests/pruners_tests/test_percentile.py
- [ ] tests/pruners_tests/test_median.py
- [ ] tests/pruners_tests/test_patient.py
- [ ] tests/pruners_tests/test_successive_halving.py
- [ ] tests/study_tests/test_optimize.py
- [ ] tests/study_tests/test_study.py
- [ ] tests/hypervolume_tests/test_hssp.py
- [x] tests/integration_tests/test_skopt.py
- [x] tests/integration_tests/test_pytorch_lightning.py
- [ ] tests/integration_tests/test_shap.py
- [ ] tests/integration_tests/test_cma.py
- [ ] tests/integration_tests/test_pytorch_distributed.py
- [ ] tests/integration_tests/lightgbm_tuner_tests/test_optimize.py
- [ ] tests/integration_tests/lightgbm_tuner_tests/test_alias.py
- [ ] tests/integration_tests/test_botorch.py
- [ ] tests/integration_tests/test_mlflow.py
- [ ] tests/integration_tests/test_mxnet.py
- [ ] tests/integration_tests/test_wandb.py
- [ ] tests/importance_tests/fanova_tests/test_tree.py
- [ ] tests/importance_tests/test_mean_decrease_impurity.py
- [ ] tests/importance_tests/test_fanova.py
- [ ] tests/importance_tests/test_init.py
- [ ] tests/test_convert_positional_args.py
- [ ] tests/test_deprecated.py
- [ ] tests/storages_tests/test_journal.py
- [ ] tests/storages_tests/test_heartbeat.py
- [ ] tests/storages_tests/test_storages.py
- [ ] tests/storages_tests/rdb_tests/test_storage.py
- [ ] tests/storages_tests/rdb_tests/create_db.py
- [ ] tests/storages_tests/test_with_server.py
- [ ] tests/samplers_tests/test_grid.py
- [ ] tests/samplers_tests/tpe_tests/test_parzen_estimator.py
- [ ] tests/samplers_tests/tpe_tests/test_multi_objective_sampler.py
- [ ] tests/samplers_tests/tpe_tests/test_sampler.py
- [ ] tests/samplers_tests/test_cmaes.py
- [ ] tests/samplers_tests/test_samplers.py
- [x] tests/samplers_tests/test_nsgaii.py
- [x] tests/samplers_tests/test_nsgaiii.py
- [ ] tests/samplers_tests/test_qmc.py
- [ ] tests/test_distributions.py
- [ ] tests/test_multi_objective.py
- [ ] tests/test_cli.py
- [ ] tests/visualization_tests/test_hypervolume_history.py
- [ ] tests/visualization_tests/test_pareto_front.py
- [ ] tests/terminator_tests/improvement_tests/test_evaluator.py
- [ ] benchmarks/kurobako/problems/wfg/transformation_functions.py
- [ ] benchmarks/bayesmark/report_bayesmark.py
- [ ] benchmarks/bayesmark/optuna_optimizer.py


### Additional context (optional)

The above list is generated by the following script.

<details>
<summary>script</summary>

```python
import os
import pathlib


PATTERS = [
    "from typing import Union",
    "from typing import Optional",
    "from typing import Tuple",
    "from typing import List",
    "from typing import Dict",
    "from typing import Set",
    "from typing import FrozenSet",
    "from typing import Type",
    "from typing import FrozenSet",
    "from typing import Sequence",
]


def get_filenames_to_be_simplified(dir_path):
    ret = []
    for f in os.listdir(dir_path):
        file_path = os.path.join(dir_path, f)
        if not os.path.isfile(file_path):
            ret.extend(get_filenames_to_be_simplified(file_path))
        else:
            try:
                with open(file_path) as fd:
                    contents = fd.read()
                    if any([s in contents for s in PATTERS]):
                        ret.append(str(file_path))
            except UnicodeDecodeError as e:
                pass
    return ret


def main():
    dirs = ["optuna", "tests", "benchmarks"]

    for dir_name in dirs:
        filenames = get_filenames_to_be_simplified(pathlib.Path(dir_name))
        for filename in filenames:
            print(f"- [ ] {filename}")

if __name__ == "__main__":
    main()
```

</details>


Discussion:
@HideakiImamura I believe the following command is all we need to close this issue:

```
pip install -U ruff && echo -n '\n[tool.ruff.isort]\nrequired-imports = ["from __future__ import annotations"]\n' >> pyproject.toml && ruff --fix --select UP007,UP006,F401,I002 optuna tests
```
A breakdown of the command:

- `pip install -U ruff`: Installs [ruff](https://github.com/charliermarsh/ruff)
- `UP007`: Fixes `Union[x, y]` to `x | y`
- `UP006`: Fixes collection types (e.g. `List[str]` -> `list[str]`)
- `F401`: Removes unused imports such as `from typing import List`
- `I002`: Enforces `from __future__ import annotations`
@harupy Thank you for your great suggestion!

----

PEP 604 seems too early for Optuna yet. It was introduced in Python 3.10.
I think `from __future__ import annotations` allows us to use `|` to define union types in < 3.10. It doesn't?
> I think from __future__ import annotations allows us to use | to define union types in < 3.10. It doesn't?

Sorry, I misunderstood something. It seems to be working.
Your commands almost work fine. But in Python 3.8, the automatic modification by `ruff` caused errors such as

```
  File "/Users/naotomizuno/optuna/optuna/study/study.py", line 46, in <module>
    ObjectiveFuncType = Callable[[trial_module.Trial], float | Sequence[float]]
TypeError: unsupported operand type(s) for |: 'type' and '_GenericAlias'
```

So, we need to review changes. Reviewing all files is huge task, PR for each file or each submodule would be appreciated.
E.g., `ruff --fix --select UP007,UP006,F401,I002 optuna/trial/_trial.py`.

----

This issue is `contribution-welcome` and `good first issue`. Contributions with small changes are welcome.
We can add `--target-version py37` to skip fixing `Callable[[trial_module.Trial], float | Sequence[float]]`.
> PR for each file or each submodule would be appreciated

Agree!
Command template:

```
echo -n '\n[tool.ruff.isort]\nrequired-imports = ["from __future__ import annotations"]\n' >> pyproject.toml && \
ruff --fix --select UP007,UP006,F401,I002 --target-version py37 REPLACE_ME && \
git restore pyproject.toml && \
isort . && \
black .
```
This issue is not yet resolved.
I made a pull request to tackle with the problem 
* https://github.com/optuna/optuna/pull/4703

I encountered the following problem while I was making the pull  request. I believe this information could potentially help others working on this issue, so I'd like to share it.


I wrote a shell script to update the files under a specific folder.

```bash
#!/bin/bash

echo -en '\n[tool.ruff.isort]\nrequired-imports = ["from __future__ import annotations"]\n' >> pyproject.toml
strings=(    
    "from typing import Union"
    "from typing import Optional"
    "from typing import Tuple"
    "from typing import List"
    "from typing import Dict"
    "from typing import Set"
    "from typing import FrozenSet"
    "from typing import Type"
    "from typing import FrozenSet"
)


folder=${1:?"Missing folder path"}


for file in $(find $folder -name "*.py"); do
    echo "Processing $file"
    for str in "${strings[@]}"; do
        if grep -q "$str" "$file"; then
            echo "$str is in the file"
            ruff --fix --select UP007,UP006,F401,I002 --target-version py37 "$file"
            break
        fi
    done
done


isort . &&  black .

git restore pyproject.toml 
```

However, 

- `benchmarks/bayesmark/report_bayesmark.py`
- `benchmarks/bayesmark/optuna_optimizer.py`
- `benchmarks/kurobako/problems/wfg/transformation_functions.py`

are not properly converted. First two files were update after I removed `--target-version py37`. The last one still required manual modificaiton on the following lines.
 
https://github.com/optuna/optuna/blob/e403f7c16a9c633b635b00c05a98dd1fb5cc2558/benchmarks/kurobako/problems/wfg/transformation_functions.py#L32-L37

I couldn't figure out the reason. 
I figured out the reason of the behavior. After all, it looks like `ruff` just worked correctly.

I misunderstood the syntax of typing alias. While typing annotation with built-in type was allowed with `from __future__ import annotations` since python3.7.  But they cannot be used in all contexts. 

If I understand correctly, the code like following is only allowed since Python3.9.

```python
from __future__ import annotations

test = tuple[int]
```
With python 3.8.16 and 3.7.16, I got `TypeError: 'type' object is not subscriptable` error.

From python3.9, all standard collections works as generics in all contexts, and the above code works without error.
 
https://peps.python.org/pep-0585/

So I think we cannot remove all of the `from typing import *` when the code includes typing alias. 

Anyway I will fix my PR based on the knowledge.

@HideakiImamura Is it acceptable if I submit multiple PRs to resolve this issue, given that my initial PR has been approved and I believe I can handle the remaining work similarly? Or would it be better to leave tasks for other contributors?





@caprest Thanks for your work. I think it is a good idea to leave this type of good first issue for contributors who want to get involved with Optuna for the first time. If you have completed one task, please feel free to work on another issue!
Some annotations from `typing` module is deprecated in [PEP584](https://peps.python.org/pep-0585/) and this requires to replace those aliases with the original generic classes. For example, 
```
from typing import Sequence
```
will be fixed as
```
from collections.abc import Sequence
```
@HideakiImamura Do we need include them in this Issues?
Sounds good. I will include them in this issue.
The list is updated.
Is the issue closed? I would be happy to contribute. Could you please assign this to me ?
@dhivyeshrk Thanks for your contribution! This is still open. Please choose one file to fix. 

Resolving patch:
diff --git a/optuna/study/_optimize.py b/optuna/study/_optimize.py
--- a/optuna/study/_optimize.py
+++ b/optuna/study/_optimize.py
@@ -1,3 +1,6 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
 from concurrent.futures import FIRST_COMPLETED
 from concurrent.futures import Future
 from concurrent.futures import ThreadPoolExecutor
@@ -9,13 +12,6 @@
 import sys
 from typing import Any
 from typing import Callable
-from typing import List
-from typing import Optional
-from typing import Sequence
-from typing import Set
-from typing import Tuple
-from typing import Type
-from typing import Union
 import warnings
 
 import optuna
@@ -37,11 +33,11 @@
 def _optimize(
     study: "optuna.Study",
     func: "optuna.study.study.ObjectiveFuncType",
-    n_trials: Optional[int] = None,
-    timeout: Optional[float] = None,
+    n_trials: int | None = None,
+    timeout: float | None = None,
     n_jobs: int = 1,
-    catch: Tuple[Type[Exception], ...] = (),
-    callbacks: Optional[List[Callable[["optuna.Study", FrozenTrial], None]]] = None,
+    catch: tuple[type[Exception], ...] = (),
+    callbacks: list[Callable[["optuna.Study", FrozenTrial], None]] | None = None,
     gc_after_trial: bool = False,
     show_progress_bar: bool = False,
 ) -> None:
@@ -80,7 +76,7 @@ def _optimize(
                 n_jobs = os.cpu_count() or 1
 
             time_start = datetime.datetime.now()
-            futures: Set[Future] = set()
+            futures: set[Future] = set()
 
             with ThreadPoolExecutor(max_workers=n_jobs) as executor:
                 for n_submitted_trials in itertools.count():
@@ -125,14 +121,14 @@ def _optimize(
 def _optimize_sequential(
     study: "optuna.Study",
     func: "optuna.study.study.ObjectiveFuncType",
-    n_trials: Optional[int],
-    timeout: Optional[float],
-    catch: Tuple[Type[Exception], ...],
-    callbacks: Optional[List[Callable[["optuna.Study", FrozenTrial], None]]],
+    n_trials: int | None,
+    timeout: float | None,
+    catch: tuple[type[Exception], ...],
+    callbacks: list[Callable[["optuna.Study", FrozenTrial], None]] | None,
     gc_after_trial: bool,
     reseed_sampler_rng: bool,
-    time_start: Optional[datetime.datetime],
-    progress_bar: Optional[pbar_module._ProgressBar],
+    time_start: datetime.datetime | None,
+    progress_bar: pbar_module._ProgressBar | None,
 ) -> None:
     # Here we set `in_optimize_loop = True`, not at the beginning of the `_optimize()` function.
     # Because it is a thread-local object and `n_jobs` option spawns new threads.
@@ -183,17 +179,17 @@ def _optimize_sequential(
 def _run_trial(
     study: "optuna.Study",
     func: "optuna.study.study.ObjectiveFuncType",
-    catch: Tuple[Type[Exception], ...],
+    catch: tuple[type[Exception], ...],
 ) -> trial_module.FrozenTrial:
     if is_heartbeat_enabled(study._storage):
         optuna.storages.fail_stale_trials(study)
 
     trial = study.ask()
 
-    state: Optional[TrialState] = None
-    value_or_values: Optional[Union[float, Sequence[float]]] = None
-    func_err: Optional[Union[Exception, KeyboardInterrupt]] = None
-    func_err_fail_exc_info: Optional[Any] = None
+    state: TrialState | None = None
+    value_or_values: float | Sequence[float] | None = None
+    func_err: Exception | KeyboardInterrupt | None = None
+    func_err_fail_exc_info: Any | None = None
 
     with get_heartbeat_thread(trial._trial_id, study._storage):
         try:
@@ -254,7 +250,7 @@ def _run_trial(
 
 def _log_failed_trial(
     trial: FrozenTrial,
-    message: Union[str, Warning],
+    message: str | Warning,
     exc_info: Any = None,
     value_or_values: Any = None,
 ) -> None:


Test patch: