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. 
Hi! This is my first comment, and I'd like to commit myself to the development of Optuna starting with tasks such as `contibution-welcome`.

However, I'm running into the first problem; when I push from my forked local repository, I get the following error. Could you please guide me what should I do?

<img width="871" alt="スクリーンショット 2024-03-14 22 33 26" src="https://github.com/optuna/optuna/assets/58361623/8f050094-250a-4224-861c-37d879dbfeac">

As in the message, you attempted to push your local changes to optuna/optuna, not your fork; please read https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo. 

Resolving patch:
diff --git a/optuna/pruners/_successive_halving.py b/optuna/pruners/_successive_halving.py
--- a/optuna/pruners/_successive_halving.py
+++ b/optuna/pruners/_successive_halving.py
@@ -1,7 +1,6 @@
+from __future__ import annotations
+
 import math
-from typing import List
-from typing import Optional
-from typing import Union
 
 import optuna
 from optuna.pruners._base import BasePruner
@@ -115,7 +114,7 @@ def objective(trial):
 
     def __init__(
         self,
-        min_resource: Union[str, int] = "auto",
+        min_resource: str | int = "auto",
         reduction_factor: int = 4,
         min_early_stopping_rate: int = 0,
         bootstrap_count: int = 0,
@@ -156,7 +155,7 @@ def __init__(
                 "are mutually incompatible, bootstrap_count is {}".format(bootstrap_count)
             )
 
-        self._min_resource: Optional[int] = None
+        self._min_resource: int | None = None
         if isinstance(min_resource, int):
             self._min_resource = min_resource
         self._reduction_factor = reduction_factor
@@ -170,7 +169,7 @@ def prune(self, study: "optuna.study.Study", trial: "optuna.trial.FrozenTrial")
 
         rung = _get_current_rung(trial)
         value = trial.intermediate_values[step]
-        trials: Optional[List["optuna.trial.FrozenTrial"]] = None
+        trials: list["optuna.trial.FrozenTrial"] | None = None
 
         while True:
             if self._min_resource is None:
@@ -215,7 +214,7 @@ def prune(self, study: "optuna.study.Study", trial: "optuna.trial.FrozenTrial")
             rung += 1
 
 
-def _estimate_min_resource(trials: List["optuna.trial.FrozenTrial"]) -> Optional[int]:
+def _estimate_min_resource(trials: list["optuna.trial.FrozenTrial"]) -> int | None:
     n_steps = [
         t.last_step for t in trials if t.state == TrialState.COMPLETE and t.last_step is not None
     ]
@@ -241,8 +240,8 @@ def _completed_rung_key(rung: int) -> str:
 
 
 def _get_competing_values(
-    trials: List["optuna.trial.FrozenTrial"], value: float, rung_key: str
-) -> List[float]:
+    trials: list["optuna.trial.FrozenTrial"], value: float, rung_key: str
+) -> list[float]:
     competing_values = [t.system_attrs[rung_key] for t in trials if rung_key in t.system_attrs]
     competing_values.append(value)
     return competing_values
@@ -250,7 +249,7 @@ def _get_competing_values(
 
 def _is_trial_promotable_to_next_rung(
     value: float,
-    competing_values: List[float],
+    competing_values: list[float],
     reduction_factor: int,
     study_direction: StudyDirection,
 ) -> bool:
diff --git a/optuna/pruners/_threshold.py b/optuna/pruners/_threshold.py
--- a/optuna/pruners/_threshold.py
+++ b/optuna/pruners/_threshold.py
@@ -1,6 +1,7 @@
+from __future__ import annotations
+
 import math
 from typing import Any
-from typing import Optional
 
 import optuna
 from optuna.pruners import BasePruner
@@ -79,8 +80,8 @@ def objective_for_lower(trial):
 
     def __init__(
         self,
-        lower: Optional[float] = None,
-        upper: Optional[float] = None,
+        lower: float | None = None,
+        upper: float | None = None,
         n_warmup_steps: int = 0,
         interval_steps: int = 1,
     ) -> None:


Test patch: