diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 187cf284b1a..32ac7f80819 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -10,12 +10,12 @@ on: env: BUILD_TYPE: core - DEFAULT_PYTHON: "3.13" + DEFAULT_PYTHON: "3.14.2" PIP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60 UV_SYSTEM_PYTHON: "true" # Base image version from https://github.com/home-assistant/docker - BASE_IMAGE_VERSION: "2025.12.0" + BASE_IMAGE_VERSION: "2026.01.0" ARCHITECTURES: '["amd64", "aarch64"]' jobs: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1002b1235e3..e86d72cc66a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,8 +41,8 @@ env: UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2026.3" - DEFAULT_PYTHON: "3.13.11" - ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']" + DEFAULT_PYTHON: "3.14.2" + ALL_PYTHON_VERSIONS: "['3.14.2']" # 10.3 is the oldest supported version # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # 10.6 is the current long-term-support diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index f963c21ae39..4f92473a39e 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -10,7 +10,7 @@ on: - "**strings.json" env: - DEFAULT_PYTHON: "3.13" + DEFAULT_PYTHON: "3.14.2" jobs: upload: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 62bfba0c5fa..a7e85354437 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ on: - "script/gen_requirements_all.py" env: - DEFAULT_PYTHON: "3.13" + DEFAULT_PYTHON: "3.14.2" concurrency: group: ${{ github.workflow }}-${{ github.ref_name}} diff --git a/.python-version b/.python-version index 24ee5b1be99..6324d401a06 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13 +3.14 diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py index 082be5787c8..a6d09e41dd2 100644 --- a/homeassistant/components/recorder/executor.py +++ b/homeassistant/components/recorder/executor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable from concurrent.futures.thread import _threads_queues, _worker -import sys import threading from typing import Any import weakref @@ -54,17 +53,10 @@ class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor): ) -> None: q.put(None) - if sys.version_info >= (3, 14): - additional_args = ( - self._create_worker_context(), - self._work_queue, - ) - else: - additional_args = ( - self._work_queue, - self._initializer, - self._initargs, - ) + additional_args = ( + self._create_worker_context(), + self._work_queue, + ) num_threads = len(self._threads) if num_threads < self._max_workers: diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 0598c1a24fb..b9fa3c524ae 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -260,7 +260,7 @@ async def get_coap_context(hass: HomeAssistant) -> COAP: ipv4: list[IPv4Address] = [] if not network.async_only_default_interface_enabled(adapters): ipv4.extend( - address + cast(IPv4Address, address) for address in await network.async_get_enabled_source_ips(hass) if address.version == 4 and not ( diff --git a/homeassistant/components/ssdp/common.py b/homeassistant/components/ssdp/common.py index 47156b13ce7..f1b961341f4 100644 --- a/homeassistant/components/ssdp/common.py +++ b/homeassistant/components/ssdp/common.py @@ -3,6 +3,7 @@ from __future__ import annotations from ipaddress import IPv4Address, IPv6Address +from typing import cast from homeassistant.components import network from homeassistant.core import HomeAssistant @@ -15,5 +16,8 @@ async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6A for source_ip in await network.async_get_enabled_source_ips(hass) if not source_ip.is_loopback and not source_ip.is_global - and ((source_ip.version == 6 and source_ip.scope_id) or source_ip.version == 4) + and ( + (source_ip.version == 6 and cast(IPv6Address, source_ip).scope_id) + or source_ip.version == 4 + ) } diff --git a/homeassistant/components/ssdp/scanner.py b/homeassistant/components/ssdp/scanner.py index 1b7d69a3214..f5b92483120 100644 --- a/homeassistant/components/ssdp/scanner.py +++ b/homeassistant/components/ssdp/scanner.py @@ -6,9 +6,9 @@ import asyncio from collections.abc import Callable, Coroutine, Mapping from datetime import timedelta from enum import Enum -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv6Address import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.const import AddressTupleVXType, DeviceOrServiceType, SsdpSource @@ -260,6 +260,7 @@ class Scanner: for source_ip in await async_build_source_set(self.hass): source_ip_str = str(source_ip) if source_ip.version == 6: + source_ip = cast(IPv6Address, source_ip) assert source_ip.scope_id is not None source_tuple: AddressTupleVXType = ( source_ip_str, diff --git a/homeassistant/components/ssdp/server.py b/homeassistant/components/ssdp/server.py index 366c6adb95b..01756d3f06b 100644 --- a/homeassistant/components/ssdp/server.py +++ b/homeassistant/components/ssdp/server.py @@ -4,10 +4,11 @@ from __future__ import annotations import asyncio from contextlib import ExitStack +from ipaddress import IPv6Address import logging import socket from time import time -from typing import Any +from typing import Any, cast from urllib.parse import urljoin import xml.etree.ElementTree as ET @@ -171,6 +172,7 @@ class Server: for source_ip in await async_build_source_set(self.hass): source_ip_str = str(source_ip) if source_ip.version == 6: + source_ip = cast(IPv6Address, source_ip) assert source_ip.scope_id is not None source_tuple: AddressTupleVXType = ( source_ip_str, diff --git a/homeassistant/components/template/validators.py b/homeassistant/components/template/validators.py index 3e2709d8401..8a33a7f35c7 100644 --- a/homeassistant/components/template/validators.py +++ b/homeassistant/components/template/validators.py @@ -165,7 +165,7 @@ def number( attribute: str, minimum: float | None = None, maximum: float | None = None, - return_type: type[float] | type[int] = float, + return_type: type[float | int] = float, **kwargs: Any, ) -> Callable[[Any], float | int | None]: """Convert the result to a number (float or int). diff --git a/homeassistant/components/velux/entity.py b/homeassistant/components/velux/entity.py index 2a50d923281..4cb56200537 100644 --- a/homeassistant/components/velux/entity.py +++ b/homeassistant/components/velux/entity.py @@ -47,7 +47,7 @@ class VeluxEntity(Entity): _attr_should_poll = False _attr_has_entity_name = True - update_callback: Callable[["Node"], Awaitable[None]] | None = None + update_callback: Callable[[Node], Awaitable[None]] | None = None _attr_available = True _unavailable_logged = False diff --git a/homeassistant/const.py b/homeassistant/const.py index 0ec2866d66a..eda30234072 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -20,7 +20,7 @@ MINOR_VERSION: Final = 3 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) +REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2) # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" diff --git a/homeassistant/runner.py b/homeassistant/runner.py index a31e58c5743..7229a971b7d 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -173,7 +173,7 @@ class RuntimeConfig: safe_mode: bool = False -class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): +class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[name-defined,misc] """Event loop policy for Home Assistant.""" def __init__(self, debug: bool) -> None: @@ -184,7 +184,7 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): @property def loop_name(self) -> str: """Return name of the loop.""" - return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined] + return self._loop_factory.__name__ # type: ignore[no-any-return] def new_event_loop(self) -> asyncio.AbstractEventLoop: """Get the event loop.""" @@ -281,7 +281,7 @@ def run(runtime_config: RuntimeConfig) -> int: """Run Home Assistant.""" _enable_posix_spawn() set_open_file_descriptor_limit() - asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) + asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) # type: ignore[deprecated] # Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out loop = asyncio.new_event_loop() try: diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 52d96109bf2..77e53298ea9 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -61,7 +61,7 @@ def run(args: list[str]) -> int: print("Aborting script, could not install dependency", req) return 1 - asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) + asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # type: ignore[deprecated] return script.run(args[1:]) diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index b034021e6e7..b734d898b64 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -48,7 +48,7 @@ def run(args: Sequence[str] | None) -> None: parser_change_pw.add_argument("new_password", type=str) parser_change_pw.set_defaults(func=change_password) - asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) + asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # type: ignore[deprecated] asyncio.run(run_command(parser.parse_args(args))) diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index c16269a2a8b..271adb5aec6 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -36,7 +36,7 @@ def run(args): args = parser.parse_args() bench = BENCHMARKS[args.name] - print("Using event loop:", asyncio.get_event_loop_policy().loop_name) + print("Using event loop:", asyncio.get_event_loop_policy().loop_name) # type: ignore[deprecated] with suppress(KeyboardInterrupt): while True: diff --git a/homeassistant/util/frozen_dataclass_compat.py b/homeassistant/util/frozen_dataclass_compat.py index a727edf88d5..45912008f7b 100644 --- a/homeassistant/util/frozen_dataclass_compat.py +++ b/homeassistant/util/frozen_dataclass_compat.py @@ -6,15 +6,11 @@ derived from EntityDescription and sub classes thereof. from __future__ import annotations +from annotationlib import Format, get_annotations import dataclasses import sys from typing import TYPE_CHECKING, Any, cast, dataclass_transform -if sys.version_info >= (3, 14): - from annotationlib import Format, get_annotations -else: - from typing_extensions import Format, get_annotations - if TYPE_CHECKING: from _typeshed import DataclassInstance @@ -103,7 +99,7 @@ class FrozenOrThawed(type): continue annotations |= get_annotations(parent, format=Format.FORWARDREF) - if "__annotations__" in cls.__dict__ or sys.version_info < (3, 14): + if "__annotations__" in cls.__dict__: cls.__annotations__ = annotations else: diff --git a/mypy.ini b/mypy.ini index dba192f88bd..51c6c69c9c7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,7 +3,7 @@ # To update, run python3 -m script.hassfest -p mypy_config [mypy] -python_version = 3.13 +python_version = 3.14 platform = linux plugins = pydantic.mypy, pydantic.v1.mypy show_error_codes = true diff --git a/pyproject.toml b/pyproject.toml index 179f648528c..bc122934138 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,10 @@ classifiers = [ "Intended Audience :: End Users/Desktop", "Intended Audience :: Developers", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Home Automation", ] -requires-python = ">=3.13.2" +requires-python = ">=3.14.2" dependencies = [ "aiodns==4.0.0", # Integrations may depend on hassio integration without listing it to @@ -102,7 +101,7 @@ include-package-data = true include = ["homeassistant*"] [tool.pylint.MAIN] -py-version = "3.13" +py-version = "3.14" # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs = 2 diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 4bc136faa68..83604411944 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -80,7 +80,7 @@ WORKDIR /config _HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest. # # To update, run python3 -m script.hassfest -p docker -FROM python:3.13-alpine +FROM python:3.14-alpine ENV \ UV_SYSTEM_PYTHON=true \ diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 69a508d7c5c..927dfdbfae0 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -1,7 +1,7 @@ # Automatically generated by hassfest. # # To update, run python3 -m script.hassfest -p docker -FROM python:3.13-alpine +FROM python:3.14-alpine ENV \ UV_SYSTEM_PYTHON=true \ diff --git a/tests/components/mill/test_climate.py b/tests/components/mill/test_climate.py index 0e72511bc11..fb6b7529183 100644 --- a/tests/components/mill/test_climate.py +++ b/tests/components/mill/test_climate.py @@ -380,7 +380,7 @@ async def test_cloud_heater( before_attrs: dict, service_name: str, service_params: dict, - effect: "contextlib.AbstractContextManager", + effect: contextlib.AbstractContextManager, heater_control_calls: list, heater_set_temp_calls: list, after_state: HVACMode, @@ -533,7 +533,7 @@ async def test_local_heater( before_attrs: dict, service_name: str, service_params: dict, - effect: "contextlib.AbstractContextManager", + effect: contextlib.AbstractContextManager, heater_mode_set_individually_calls: list, heater_mode_set_off_calls: list, heater_set_target_temperature_calls: list, diff --git a/tests/components/thermopro/test_sensor.py b/tests/components/thermopro/test_sensor.py index f42baa0b343..2d6e9b87e33 100644 --- a/tests/components/thermopro/test_sensor.py +++ b/tests/components/thermopro/test_sensor.py @@ -139,7 +139,7 @@ async def test_sensors(hass: HomeAssistant) -> None: class CoordinatorStub: """Coordinator stub for testing entity restoration behavior.""" - instances: list["CoordinatorStub"] = [] + instances: list[CoordinatorStub] = [] def __init__( self,