diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 07b4b3f03020..c33182742390 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,28 +31,22 @@ jobs: include: # Make sure to run mypyc compiled unit tests for both # the oldest and newest supported Python versions - - name: Test suite with py39-ubuntu, mypyc-compiled - python: '3.9' - os: ubuntu-24.04-arm - toxenv: py - tox_extra_args: "-n 4" - test_mypyc: true - - name: Test suite with py310-ubuntu + - name: Test suite with py310-ubuntu, mypyc-compiled python: '3.10' os: ubuntu-24.04-arm toxenv: py tox_extra_args: "-n 4" + test_mypyc: true - name: Test suite with py311-ubuntu python: '3.11' os: ubuntu-24.04-arm toxenv: py tox_extra_args: "-n 4" - - name: Test suite with py312-ubuntu, mypyc-compiled + - name: Test suite with py312-ubuntu python: '3.12' os: ubuntu-24.04-arm toxenv: py tox_extra_args: "-n 4" - test_mypyc: true - name: Test suite with py313-ubuntu, mypyc-compiled python: '3.13' os: ubuntu-24.04-arm @@ -101,12 +95,12 @@ jobs: # tox_extra_args: "-n 4 mypyc/test/test_run.py mypyc/test/test_external.py" # debug_build: true - - name: Type check our own code (py39-ubuntu) - python: '3.9' + - name: Type check our own code (py310-ubuntu) + python: '3.10' os: ubuntu-latest toxenv: type - - name: Type check our own code (py39-windows-64) - python: '3.9' + - name: Type check our own code (py310-windows-64) + python: '3.10' os: windows-latest toxenv: type diff --git a/.github/workflows/test_stubgenc.yml b/.github/workflows/test_stubgenc.yml index 6cf3cb71c3ff..7102b6513ddc 100644 --- a/.github/workflows/test_stubgenc.yml +++ b/.github/workflows/test_stubgenc.yml @@ -32,10 +32,10 @@ jobs: with: persist-credentials: false - - name: Setup 🐍 3.9 + - name: Setup 🐍 3.10 uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: '3.10' - name: Test stubgenc run: misc/test-stubgenc.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 134d251d90b1..33770c178d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Next Release +### Drop Support for Python 3.9 + +Mypy no longer supports running with Python 3.9, which has reached end-of-life. +When running mypy with Python 3.10+, it is still possible to type check code +that needs to support Python 3.9 with the `--python-version 3.9` argument. +Support for this will be dropped in the first half of 2026! + +Contributed by Marc Mueller (PR [20156](https://github.com/python/mypy/pull/20156)). + ## Mypy 1.18.1 We’ve just uploaded mypy 1.18.1 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d7dd2d1e886..28828482706d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ hash -r # This resets shell PATH cache, not necessary on Windows ``` > **Note** -> You'll need Python 3.9 or higher to install all requirements listed in +> You'll need Python 3.10 or higher to install all requirements listed in > test-requirements.txt ### Running tests diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 9b510314fd8f..2a578f974864 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -16,7 +16,7 @@ may not make much sense otherwise. Installing and running mypy *************************** -Mypy requires Python 3.9 or later to run. You can install mypy using pip: +Mypy requires Python 3.10 or later to run. You can install mypy using pip: .. code-block:: shell diff --git a/mypy/defaults.py b/mypy/defaults.py index 58a74a478b16..eca6f714f145 100644 --- a/mypy/defaults.py +++ b/mypy/defaults.py @@ -6,7 +6,7 @@ # Earliest fully supported Python 3.x version. Used as the default Python # version in tests. Mypy wheels should be built starting with this version, # and CI tests should be run on this version (and later versions). -PYTHON3_VERSION: Final = (3, 9) +PYTHON3_VERSION: Final = (3, 10) # Earliest Python 3.x version supported via --python-version 3.x. To run # mypy, at least version PYTHON3_VERSION is needed. diff --git a/mypy/test/meta/test_parse_data.py b/mypy/test/meta/test_parse_data.py index 8c6fc1610e63..3261fcd51371 100644 --- a/mypy/test/meta/test_parse_data.py +++ b/mypy/test/meta/test_parse_data.py @@ -50,13 +50,15 @@ def test_bad_ge_version_check(self) -> None: """ [case abc] s: str - [out version>=3.9] + [out version>=3.10] abc """ ) # Assert - assert "version>=3.9 always true since minimum runtime version is (3, 9)" in actual.stdout + assert ( + "version>=3.10 always true since minimum runtime version is (3, 10)" in actual.stdout + ) def test_bad_eq_version_check(self) -> None: # Act @@ -70,4 +72,6 @@ def test_bad_eq_version_check(self) -> None: ) # Assert - assert "version==3.7 always false since minimum runtime version is (3, 9)" in actual.stdout + assert ( + "version==3.7 always false since minimum runtime version is (3, 10)" in actual.stdout + ) diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py index 6d22aca07da7..2583de92798c 100644 --- a/mypy/test/testpythoneval.py +++ b/mypy/test/testpythoneval.py @@ -71,6 +71,9 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None return mypy_cmdline.extend(additional_flags) + if "--no-force-union-syntax" not in mypy_cmdline: + mypy_cmdline.append("--force-union-syntax") + # Write the program to a file. program = "_" + testcase.name + ".py" program_path = os.path.join(test_temp_dir, program) diff --git a/mypy/util.py b/mypy/util.py index c919ff87f5b0..52ee11de8fc3 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -482,10 +482,10 @@ def get_unique_redefinition_name(name: str, existing: Container[str]) -> str: def check_python_version(program: str) -> None: """Report issues with the Python used to run mypy, dmypy, or stubgen""" # Check for known bad Python versions. - if sys.version_info[:2] < (3, 9): # noqa: UP036, RUF100 + if sys.version_info[:2] < (3, 10): # noqa: UP036, RUF100 sys.exit( - "Running {name} with Python 3.8 or lower is not supported; " - "please upgrade to 3.9 or newer".format(name=program) + "Running {name} with Python 3.9 or lower is not supported; " + "please upgrade to 3.10 or newer".format(name=program) ) diff --git a/mypy_self_check.ini b/mypy_self_check.ini index 0b49b3de862b..c52acb87f869 100644 --- a/mypy_self_check.ini +++ b/mypy_self_check.ini @@ -7,7 +7,7 @@ show_traceback = True pretty = True always_false = MYPYC plugins = mypy.plugins.proper_plugin -python_version = 3.9 +python_version = 3.10 exclude = mypy/typeshed/|mypyc/test-data/|mypyc/lib-rt/ enable_error_code = ignore-without-code,redundant-expr enable_incomplete_feature = PreciseTupleTypes diff --git a/mypyc/doc/getting_started.rst b/mypyc/doc/getting_started.rst index f85981f08d02..61b41908c544 100644 --- a/mypyc/doc/getting_started.rst +++ b/mypyc/doc/getting_started.rst @@ -38,7 +38,7 @@ Installation ------------ Mypyc is shipped as part of the mypy distribution. Install mypy like -this (you need Python 3.9 or later): +this (you need Python 3.10 or later): .. code-block:: diff --git a/pyproject.toml b/pyproject.toml index 42e10967cba2..5be709a13030 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -47,7 +46,7 @@ classifiers = [ "Topic :: Software Development", "Typing :: Typed", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ # When changing this, also update build-system.requires and mypy-requirements.txt "typing_extensions>=4.6.0", @@ -107,7 +106,7 @@ mypyc = [ [tool.black] line-length = 99 -target-version = ["py39", "py310", "py311", "py312", "py313", "py314"] +target-version = ["py310", "py311", "py312", "py313", "py314"] skip-magic-trailing-comma = true force-exclude = ''' ^/mypy/typeshed| diff --git a/setup.py b/setup.py index 0037624f9bbc..8f8cf051ee2e 100644 --- a/setup.py +++ b/setup.py @@ -8,8 +8,8 @@ import sys from typing import TYPE_CHECKING, Any -if sys.version_info < (3, 9, 0): # noqa: UP036, RUF100 - sys.stderr.write("ERROR: You need Python 3.9 or later to use mypy.\n") +if sys.version_info < (3, 10, 0): # noqa: UP036, RUF100 + sys.stderr.write("ERROR: You need Python 3.10 or later to use mypy.\n") exit(1) # we'll import stuff from the source tree, let's ensure is on the sys path diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index 35d7b700b161..a0698746358f 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1038,10 +1038,6 @@ public static void main(String[] args) [file pkg/y.py] x: str = 0 [out] -pkg/x.py:1: error: Invalid syntax -Found 1 error in 1 file (errors prevented further checking) -== Return code: 2 -[out version>=3.10] pkg/x.py:1: error: Invalid syntax. Perhaps you forgot a comma? Found 1 error in 1 file (errors prevented further checking) == Return code: 2 diff --git a/test-data/unit/fine-grained-blockers.test b/test-data/unit/fine-grained-blockers.test index 8e16da053d6a..0b2d9d2fcb5f 100644 --- a/test-data/unit/fine-grained-blockers.test +++ b/test-data/unit/fine-grained-blockers.test @@ -19,12 +19,6 @@ def f(x: int) -> None: pass def f() -> None: pass [out] == -a.py:1: error: Invalid syntax -== -main:2: error: Missing positional argument "x" in call to "f" -== -[out version>=3.10] -== a.py:1: error: Expected ':' == main:2: error: Missing positional argument "x" in call to "f" @@ -44,16 +38,6 @@ def f(x: int) -> None: pass def f() -> None: pass [out] == -a.py:1: error: Invalid syntax [syntax] - def f(x: int) -> - ^ -== -main:3: error: Missing positional argument "x" in call to "f" [call-arg] - a.f() - ^~~~~ -== -[out version>=3.10] -== a.py:1: error: Expected ':' [syntax] def f(x: int) -> ^ @@ -77,13 +61,6 @@ def f(x: int def f(x: int) -> None: pass [out] == -a.py:1: error: Invalid syntax -== -a.py:2: error: Invalid syntax -== -main:2: error: Missing positional argument "x" in call to "f" -[out version>=3.10] -== a.py:1: error: Expected ':' == a.py:2: error: Expected ':' @@ -124,14 +101,6 @@ def f() -> None: pass main:3: error: Too many arguments for "f" main:5: error: Too many arguments for "f" == -a.py:1: error: Invalid syntax -== -main:3: error: Too many arguments for "f" -main:5: error: Too many arguments for "f" -[out version>=3.10] -main:3: error: Too many arguments for "f" -main:5: error: Too many arguments for "f" -== a.py:1: error: Expected ':' == main:3: error: Too many arguments for "f" diff --git a/test-data/unit/fine-grained-suggest.test b/test-data/unit/fine-grained-suggest.test index c2e544baf38b..ce57514276a2 100644 --- a/test-data/unit/fine-grained-suggest.test +++ b/test-data/unit/fine-grained-suggest.test @@ -1117,11 +1117,6 @@ def foo(): ( [out] -foo.py:4: error: Unexpected EOF while parsing -Command 'suggest' is only valid after a 'check' command (that produces no parse errors) -== -foo.py:4: error: Unexpected EOF while parsing -[out version>=3.10] foo.py:4: error: '(' was never closed Command 'suggest' is only valid after a 'check' command (that produces no parse errors) == diff --git a/test-data/unit/parse-errors.test b/test-data/unit/parse-errors.test index a192cc02d0cc..99baf519e31c 100644 --- a/test-data/unit/parse-errors.test +++ b/test-data/unit/parse-errors.test @@ -154,8 +154,6 @@ file:2: error: Syntax error in type comment "A B" [case testMissingBracket] def foo( [out] -file:1: error: Unexpected EOF while parsing -[out version>=3.10] file:1: error: '(' was never closed [case testInvalidSignatureInComment1] diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 2069d082df17..6de4780747da 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1651,8 +1651,8 @@ def foo(x: T) -> T: return x [out] _testTypeAliasWithNewStyleUnion.py:5: note: Revealed type is "typing._SpecialForm" -_testTypeAliasWithNewStyleUnion.py:25: note: Revealed type is "type[builtins.int] | builtins.str" -_testTypeAliasWithNewStyleUnion.py:28: note: Revealed type is "type[builtins.int] | builtins.str" +_testTypeAliasWithNewStyleUnion.py:25: note: Revealed type is "Union[type[builtins.int], builtins.str]" +_testTypeAliasWithNewStyleUnion.py:28: note: Revealed type is "Union[type[builtins.int], builtins.str]" [case testTypeAliasWithNewStyleUnionInStub] import m @@ -1704,7 +1704,7 @@ CU3 = int | Callable[[str | bool], str] CU4: TypeAlias = int | Callable[[str | bool], str] [out] m.pyi:5: note: Revealed type is "typing._SpecialForm" -m.pyi:22: note: Revealed type is "typing._SpecialForm" +m.pyi:22: note: Revealed type is "types.UnionType[type[builtins.int], builtins.str]" _testTypeAliasWithNewStyleUnionInStub.py:3: note: Revealed type is "Union[type[builtins.int], builtins.str]" _testTypeAliasWithNewStyleUnionInStub.py:5: note: Revealed type is "Union[type[builtins.int], builtins.str]" _testTypeAliasWithNewStyleUnionInStub.py:7: note: Revealed type is "Union[type[builtins.int], builtins.str]" @@ -1730,7 +1730,7 @@ reveal_type(e.foo) reveal_type(E.Y.foo) [out] _testEnumNameWorkCorrectlyOn311.py:11: note: Revealed type is "builtins.str" -_testEnumNameWorkCorrectlyOn311.py:12: note: Revealed type is "Literal[1]? | Literal[2]?" +_testEnumNameWorkCorrectlyOn311.py:12: note: Revealed type is "Union[Literal[1]?, Literal[2]?]" _testEnumNameWorkCorrectlyOn311.py:13: note: Revealed type is "Literal['X']?" _testEnumNameWorkCorrectlyOn311.py:14: note: Revealed type is "builtins.int" _testEnumNameWorkCorrectlyOn311.py:15: note: Revealed type is "builtins.int" @@ -1799,9 +1799,9 @@ WrongEllipsis = tuple[float, float, ...] | str # Error reveal_type(tuple[int, str]((1, "x"))) [out] -_testTupleWithDifferentArgsPy310.py:15: note: Revealed type is "builtins.str | tuple[builtins.float, builtins.float, builtins.str]" -_testTupleWithDifferentArgsPy310.py:16: note: Revealed type is "tuple[builtins.float] | builtins.str" -_testTupleWithDifferentArgsPy310.py:17: note: Revealed type is "builtins.tuple[builtins.float, ...] | builtins.str" +_testTupleWithDifferentArgsPy310.py:15: note: Revealed type is "Union[builtins.str, tuple[builtins.float, builtins.float, builtins.str]]" +_testTupleWithDifferentArgsPy310.py:16: note: Revealed type is "Union[tuple[builtins.float], builtins.str]" +_testTupleWithDifferentArgsPy310.py:17: note: Revealed type is "Union[builtins.tuple[builtins.float, ...], builtins.str]" _testTupleWithDifferentArgsPy310.py:18: note: Revealed type is "tuple[builtins.float, builtins.str]" _testTupleWithDifferentArgsPy310.py:19: note: Revealed type is "builtins.tuple[builtins.float, ...]" _testTupleWithDifferentArgsPy310.py:20: note: Revealed type is "builtins.list[tuple[builtins.int, builtins.str]]" diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index 8053b33b94fd..2526d633fbb8 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -361,15 +361,11 @@ main:2: error: "yield" outside function [case testInvalidLvalues1] 1 = 1 [out] -main:1: error: Cannot assign to literal -[out version>=3.10] main:1: error: Cannot assign to literal here. Maybe you meant '==' instead of '='? [case testInvalidLvalues2] (1) = 1 [out] -main:1: error: Cannot assign to literal -[out version>=3.10] main:1: error: Cannot assign to literal here. Maybe you meant '==' instead of '='? [case testInvalidLvalues3] @@ -408,36 +404,26 @@ main:3: error: Cannot assign to literal [case testInvalidLvalues10] x + x = 1 [out] -main:1: error: Cannot assign to operator -[out version>=3.10] main:1: error: Cannot assign to expression here. Maybe you meant '==' instead of '='? [case testInvalidLvalues11] -x = 1 [out] -main:1: error: Cannot assign to operator -[out version>=3.10] main:1: error: Cannot assign to expression here. Maybe you meant '==' instead of '='? [case testInvalidLvalues12] 1.1 = 1 [out] -main:1: error: Cannot assign to literal -[out version>=3.10] main:1: error: Cannot assign to literal here. Maybe you meant '==' instead of '='? [case testInvalidLvalues13] 'x' = 1 [out] -main:1: error: Cannot assign to literal -[out version>=3.10] main:1: error: Cannot assign to literal here. Maybe you meant '==' instead of '='? [case testInvalidLvalues14] x() = 1 [out] -main:1: error: Cannot assign to function call -[out version>=3.10] main:1: error: Cannot assign to function call here. Maybe you meant '==' instead of '='? [case testTwoStarExpressions] @@ -498,8 +484,6 @@ main:2: error: Cannot delete function call x = 1 del x + 1 [out] -main:2: error: Cannot delete operator -[out version>=3.10] main:2: error: Cannot delete expression [case testInvalidDel3] @@ -881,8 +865,6 @@ import typing def f(): pass f() = 1 # type: int [out] -main:3: error: Cannot assign to function call -[out version>=3.10] main:3: error: Cannot assign to function call here. Maybe you meant '==' instead of '='? [case testIndexedAssignmentWithTypeDeclaration] @@ -959,8 +941,6 @@ x, y = 1, 2 # type: int # E: Tuple type expected for multiple variables a = 1 a() = None # type: int [out] -main:2: error: Cannot assign to function call -[out version>=3.10] main:2: error: Cannot assign to function call here. Maybe you meant '==' instead of '='? [case testInvalidLvalueWithExplicitType2] @@ -1303,8 +1283,6 @@ import typing def f() -> None: f() = 1 # type: int [out] -main:3: error: Cannot assign to function call -[out version>=3.10] main:3: error: Cannot assign to function call here. Maybe you meant '==' instead of '='? [case testInvalidReferenceToAttributeOfOuterClass] diff --git a/test-data/unit/semanal-statements.test b/test-data/unit/semanal-statements.test index a2e8691733ef..58a073e0507b 100644 --- a/test-data/unit/semanal-statements.test +++ b/test-data/unit/semanal-statements.test @@ -557,8 +557,6 @@ MypyFile:1( def f(x, y) -> None: del x, y + 1 [out] -main:2: error: Cannot delete operator -[out version>=3.10] main:2: error: Cannot delete expression [case testTry] diff --git a/test-requirements.in b/test-requirements.in index 556edf5077d2..78d9863dc9e6 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -4,13 +4,12 @@ -r mypy-requirements.txt -r build-requirements.txt attrs>=18.0 -filelock>=3.3.0,<3.20.0 # latest version is not available on 3.9 that we still support +filelock>=3.3.0 lxml>=5.3.0; python_version<'3.15' psutil>=4.0 pytest>=8.1.0 pytest-xdist>=1.34.0 pytest-cov>=2.10.0 setuptools>=75.1.0 -tomli>=1.1.0 # needed even on py311+ so the self check passes with --python-version 3.9 +tomli>=1.1.0 # needed even on py311+ so the self check passes with --python-version 3.10 pre_commit>=3.5.0 -platformdirs<4.5.0 # latest version is not available on 3.9 that we still support diff --git a/test-requirements.txt b/test-requirements.txt index b9ff4ffe085b..a89dc981a445 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --allow-unsafe --output-file=test-requirements.txt --strip-extras test-requirements.in @@ -14,7 +14,7 @@ distlib==0.4.0 # via virtualenv execnet==2.1.1 # via pytest-xdist -filelock==3.19.1 +filelock==3.20.0 # via # -r test-requirements.in # virtualenv @@ -34,10 +34,8 @@ packaging==25.0 # via pytest pathspec==0.12.1 # via -r mypy-requirements.txt -platformdirs==4.4.0 - # via - # -r test-requirements.in - # virtualenv +platformdirs==4.5.0 + # via virtualenv pluggy==1.6.0 # via # pytest diff --git a/tox.ini b/tox.ini index 65f67aba42a2..8e97590b0d2a 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,6 @@ minversion = 4.4.4 skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} envlist = - py38, - py39, py310, py311, py312,