RFC Write an explicit rule about bumping our minimum dependencies

Roughly a year ago, [SPEC0](https://scientific-python.org/specs/spec-0000/) was rejected following a vote and we said we would write our own rule, but we did not 😅. 

Until now 💪.

This was spurred by a [Discord discussion](https://discord.com/channels/731163543038197871/1046822941586898974/1338208487657701446) with @lucascolley, @betatim, @jeremiedbb and @ogrisel.

cc @glemaitre whom I had a chat with about this.

### Proposed rule

- **Python**: in each scikit-learn December release, we bump our minimum supported Python to the Python version that was released a bit more than 3 years ago (Python releases happened yearly beginning of October). 
- **non pure-Python dependencies** (numpy, scipy, pandas, etc ...): in each December release they are bumped to the minimum minor release that has wheels for the minimum Python version. 
- **pure Python dependencies**: in each release (December and June) bump to the most recent minor release older than 2 years old
- we expect that exceptions may arise, although hopefully not too often, for example security or critical bug fixes

### Rationale

- we want a simple rule
- we don't want to be even more conservative that what we have been doing historically
- in an ideal world, we would want to try to avoid requiring newer versions if there is not a "good reason" too, although there is some tension between having a "simple rule" and this bullet point

### Proposed plan

we didn't bump our dependency in 1.6 so we would bump them in 1.7 (June 2025) and start the regular December version bump in December 2025.

This is what it would look like for the next 4 scikit-learn releases (`python-date-diff` column is the age of the min Python at the time of the scikit-learn release, and similarly for other dependencies).

Python:
```
  scikit-learn scikit-learn-date python  python-date-diff
0          1.7        2025-06-01   3.10          3.660274
1          1.8        2025-12-01   3.11          3.106849
2          1.9        2026-06-01   3.11          3.605479
3         1.10        2026-12-01   3.12          3.167123
```

non pure-Python dependencies:
```
  scikit-learn scikit-learn-date numpy  numpy-date-diff scipy  scipy-date-diff pandas  pandas-date-diff
0          1.7        2025-06-01  1.22         3.416438   1.8         3.317808    1.4          3.356164
1          1.8        2025-12-01  1.24         2.953425  1.11         2.435616    2.0          2.663014
2          1.9        2026-06-01  1.24         3.452055  1.11         2.934247    2.0          3.161644
3         1.10        2026-12-01  1.26         3.208219  1.12         2.863014    2.2          2.863014
```

pure Python dependencies:
```
  scikit-learn scikit-learn-date joblib  joblib-date-diff threadpoolctl  threadpoolctl-date-diff
0          1.7        2025-06-01    1.2          2.706849           3.1                 3.331507
1          1.8        2025-12-01    1.3          2.427397           3.2                 2.386301
2          1.9        2026-06-01    1.4          2.145205           3.5                 2.087671
3         1.10        2026-12-01    1.4          2.646575           3.5                 2.589041
```


### Comparison to SPEC0

- Python: the proposed rule corresponds to dropping Python support after 4 years (instead of 3 years for SPEC0)
- non pure-Python dependencies: This corresponds to dropping support for a version that is a bit more than 3 years old (rather than 2 years for SPEC0).
- pure-Python dependencies: same as SPEC0

Note: SPEC0 talks about the age of the dropped dependency release, which I find slightly harder to think about than the age of the minimum depency release. The age of the dropped dependency release also depends on the dependency release cadence. This does not matter than much for dependencies with regular releases like numpy, but makes more of a difference for dependencies with less regular releases, like joblib.

### "What is our implicit rule?" investigation

tl; dr: very ad-hoc and sometimes more adventurous than the proposed rule

I used a quick and dirty [script](https://gist.github.com/lesteve/6099307249f3235e16063ad847ff0348) to figure out what we have been doing historically.

Python:
```
  scikit-learn python  python-date-diff
0         0.21    3.5          3.657534
1         0.22    3.5          4.224658
2         0.23    3.6          3.386301
3          1.0    3.7          3.246575
4          1.1    3.8          2.578082
5          1.2    3.8          3.153425
6          1.3    3.8          3.712329
7          1.4    3.9          3.287671
8          1.5    3.9          3.627397
9          1.6    3.9          4.180822
```

the proposed rule means our minimum Python is a bit more than 3 years old, which fits what we have done historically. Note that scikit-learn 1.1 dropped 3.7 quite aggressively (https://github.com/scikit-learn/scikit-learn/pull/22617 does not mention any particular reason), and that we did not bump our dependency for 1.6.

numpy:
```
  scikit-learn python   numpy  numpy-date-diff
0         0.21    3.5  1.11.0         3.117808
1         0.22    3.5  1.11.0         3.684932
2         0.23    3.6  1.13.3         2.616438
3          1.0    3.7  1.14.6         3.002740
4          1.1    3.8  1.17.3         2.567123
5          1.2    3.8  1.17.3         3.142466
6          1.3    3.8  1.17.3         3.701370
7          1.4    3.9  1.19.5         3.032877
8          1.5    3.9  1.19.5         3.372603
9          1.6    3.9  1.19.5         3.926027
```
roughly 3 years in terms of time-based rule, which roughly matches what the proposed rule would do. Note the aggressive bump in 1.1.

scipy:
```
  scikit-learn python   scipy  scipy-date-diff
0         0.21    3.5  0.17.0         3.293151
1         0.22    3.5  0.17.0         3.860274
2         0.23    3.6  0.19.1         2.887671
3          1.0    3.7   1.1.0         3.389041
4          1.1    3.8   1.3.2         2.504110
5          1.2    3.8   1.3.2         3.079452
6          1.3    3.8   1.5.0         3.021918
7          1.4    3.9   1.6.0         3.046575
8          1.5    3.9   1.6.0         3.389041
9          1.6    3.9   1.6.0         3.942466
```
roughly 3 years in terms of time-based rule, which roughly matches what the proposed rule would do. Note the aggressive bump in 1.1.

pandas:
```
  scikit-learn python  pandas  scipy-date-diff
0         0.21    3.5     NaN         3.293151
1         0.22    3.5     NaN         3.860274
2         0.23    3.6     NaN         2.887671
3          1.0    3.7  0.25.0         3.389041
4          1.1    3.8   1.0.5         2.504110
5          1.2    3.8   1.0.5         3.079452
6          1.3    3.8   1.0.5         3.021918
7          1.4    3.9   1.1.5         3.046575
8          1.5    3.9   1.1.5         3.389041
9          1.6    3.9   1.1.5         3.942466
```
roughly 3 years in terms of time-based rule, which roughly matches what the proposed rule would do. Note the aggressive bump in 1.1.

joblib:
```
  scikit-learn python joblib  joblib-date-diff
0         0.21    3.5   0.11          2.186301
1         0.22    3.5   0.11          2.756164
2         0.23    3.6   0.11          3.197260
3          1.0    3.7   0.11          4.567123
4          1.1    3.8  1.0.0          1.408219
5          1.2    3.8  1.1.1          0.161644
6          1.3    3.8  1.1.1          0.717808
7          1.4    3.9  1.2.0          1.336986
8          1.5    3.9  1.2.0          1.679452
9          1.6    3.9  1.2.0          2.232877
```
the proposed rule (2 years) roughly corresponds to what we have been doing when deciding to bump. Note:
- aggressive bumping in 1.1: joblib min version was less than 1.5 year old
- bumping in 1.2 was due to a security bug-fix

```
  scikit-learn python threadpoolctl  threadpoolctl-date-diff
0         0.21    3.5           NaN                      NaN
1         0.22    3.5           NaN                      NaN
2         0.23    3.6           NaN                      NaN
3          1.0    3.7         2.0.0                 1.802740
4          1.1    3.8         2.0.0                 2.432877
5          1.2    3.8         2.0.0                 3.008219
6          1.3    3.8         2.0.0                 3.567123
7          1.4    3.9         2.0.0                 4.120548
8          1.5    3.9         3.1.0                 2.304110
9          1.6    3.9         3.1.0                 2.854795
```
The proposed rule (2 years) roughly corresponds to what we have been doing historically when deciding to bump.
- bumping in 1.5 was done with the "more than 2 years old is OK" argument https://github.com/scikit-learn/scikit-learn/pull/28838#issue-2243861595.

COMMENT:
Speaking with @lesteve in real life and looking at the proposal, I'm fine with it.

I really think that there is value in having a statement instead of going back each time we need to move forward with version bumping. To me, it is a strong proposal because:

- it makes a retrospective and applies rules that we implicitly followed in the past;
- the fact that it looks at other libraries and their availability for a specific Python version is pragmatic: if a user tries to install a version not available with wheels, the experience will not be great. So it makes total sense to me because this rational argument is based on user experience.

Labels: RFC