From d890abb40d03c3dc83f8602d53195f4f97250b92 Mon Sep 17 00:00:00 2001
From: David Peter
Date: Wed, 8 Oct 2025 10:03:58 +0200
Subject: [PATCH 1/2] [ty] Use 3.14 as the default version
---
crates/ruff_python_ast/src/python_version.rs | 4 ++--
crates/ty/docs/cli.md | 2 +-
crates/ty/docs/configuration.md | 4 ++--
crates/ty/src/args.rs | 2 +-
crates/ty_ide/src/completion.rs | 6 +++++-
crates/ty_project/src/metadata/options.rs | 4 ++--
crates/ty_python_semantic/src/types/class.rs | 8 --------
crates/ty_test/src/config.rs | 2 +-
8 files changed, 14 insertions(+), 18 deletions(-)
diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs
index 2cedb435fd592..82f34f2a48456 100644
--- a/crates/ruff_python_ast/src/python_version.rs
+++ b/crates/ruff_python_ast/src/python_version.rs
@@ -67,8 +67,8 @@ impl PythonVersion {
}
pub const fn latest_ty() -> Self {
- // Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
- Self::PY313
+ // Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
+ Self::PY314
}
pub const fn as_tuple(self) -> (u8, u8) {
diff --git a/crates/ty/docs/cli.md b/crates/ty/docs/cli.md
index f9f5580f2b408..4c44f523d79e3 100644
--- a/crates/ty/docs/cli.md
+++ b/crates/ty/docs/cli.md
@@ -76,7 +76,7 @@ over all configuration files.
This is used to specialize the type of sys.platform and will affect the visibility of platform-specific functions and attributes. If the value is set to all, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.
The Python version affects allowed syntax, type definitions of the standard library, and type definitions of first- and third-party modules that are conditional on the Python version.
-
If a version is not specified on the command line or in a configuration file, ty will try the following techniques in order of preference to determine a value: 1. Check for the project.requires-python setting in a pyproject.toml file and use the minimum version from the specified range 2. Check for an activated or configured Python environment and attempt to infer the Python version of that environment 3. Fall back to the latest stable Python version supported by ty (currently Python 3.13)
+
If a version is not specified on the command line or in a configuration file, ty will try the following techniques in order of preference to determine a value: 1. Check for the project.requires-python setting in a pyproject.toml file and use the minimum version from the specified range 2. Check for an activated or configured Python environment and attempt to infer the Python version of that environment 3. Fall back to the latest stable Python version supported by ty (see ty check --help output)
Possible values:
3.7
diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md
index 248e0f547c0df..4ecf8f8399f0d 100644
--- a/crates/ty/docs/configuration.md
+++ b/crates/ty/docs/configuration.md
@@ -133,9 +133,9 @@ For some language features, ty can also understand conditionals based on compari
with `sys.version_info`. These are commonly found in typeshed, for example,
to reflect the differing contents of the standard library across Python versions.
-**Default value**: `"3.13"`
+**Default value**: `"3.14"`
-**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | .`
+**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | .`
**Example usage** (`pyproject.toml`):
diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs
index 8ec32c8c63a95..f1be98ea46bf7 100644
--- a/crates/ty/src/args.rs
+++ b/crates/ty/src/args.rs
@@ -85,7 +85,7 @@ pub(crate) struct CheckCommand {
/// and use the minimum version from the specified range
/// 2. Check for an activated or configured Python environment
/// and attempt to infer the Python version of that environment
- /// 3. Fall back to the latest stable Python version supported by ty (currently Python 3.13)
+ /// 3. Fall back to the latest stable Python version supported by ty (see `ty check --help` output)
#[arg(long, value_name = "VERSION", alias = "target-version")]
pub(crate) python_version: Option,
diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs
index 32c3ef02e190b..933b940dc4caf 100644
--- a/crates/ty_ide/src/completion.rs
+++ b/crates/ty_ide/src/completion.rs
@@ -1732,6 +1732,7 @@ C.
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: int
mro :: bound method .mro() -> list[type]
+ __annotate__ :: @Todo | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
@@ -1802,6 +1803,7 @@ Meta.
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: property
mro :: def mro(self) -> list[type]
+ __annotate__ :: @Todo(Support for `typing.TypeAlias`) | None
__base__ :: type | None
__bases__ :: tuple[type, ...]
__basicsize__ :: int
@@ -1908,6 +1910,7 @@ Quux.
some_method :: def some_method(self) -> int
some_property :: property
some_static_method :: def some_static_method(self) -> int
+ __annotate__ :: @Todo | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
@@ -1978,6 +1981,7 @@ Answer.
mro :: bound method .mro() -> list[type]
name :: Any
value :: Any
+ __annotate__ :: @Todo(Support for `typing.TypeAlias`) | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
@@ -2020,7 +2024,7 @@ Answer.
__reversed__ :: bound method .__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
__ror__ :: bound method .__ror__(value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
- __signature__ :: bound method .__signature__() -> str
+ __signature__ :: @Todo(Support for `typing.TypeAlias`)
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method .__subclasscheck__(subclass: type, /) -> bool
diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs
index 36709174df9ed..12b76affc17a5 100644
--- a/crates/ty_project/src/metadata/options.rs
+++ b/crates/ty_project/src/metadata/options.rs
@@ -520,8 +520,8 @@ pub struct EnvironmentOptions {
/// to reflect the differing contents of the standard library across Python versions.
#[serde(skip_serializing_if = "Option::is_none")]
#[option(
- default = r#""3.13""#,
- value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | ."#,
+ default = r#""3.14""#,
+ value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | ."#,
example = r#"
python-version = "3.12"
"#
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index ae44c0cebd10c..896c2730de0b1 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -5510,14 +5510,6 @@ mod tests {
});
for class in KnownClass::iter() {
- // Until the latest supported version is bumped to Python 3.14
- // we need to skip template strings here.
- // The assertion below should remind the developer to
- // remove this exception once we _do_ bump `latest_ty`
- assert_ne!(PythonVersion::latest_ty(), PythonVersion::PY314);
- if matches!(class, KnownClass::Template) {
- continue;
- }
assert_ne!(
class.to_instance(&db),
Type::unknown(),
diff --git a/crates/ty_test/src/config.rs b/crates/ty_test/src/config.rs
index bc32678449eb8..6fe3a17fd0537 100644
--- a/crates/ty_test/src/config.rs
+++ b/crates/ty_test/src/config.rs
@@ -63,7 +63,7 @@ pub(crate) struct Environment {
///
/// By default, the Python version is inferred as the lower bound of the project's
/// `requires-python` field from the `pyproject.toml`, if available. Otherwise, the latest
- /// stable version supported by ty is used, which is currently 3.13.
+ /// stable version supported by ty is used (see `ty check --help` output).
///
/// ty will not infer the Python version from the Python environment at this time.
pub(crate) python_version: Option,
From 95de6b8593093916e9129240473a115f7ac7b941 Mon Sep 17 00:00:00 2001
From: David Peter
Date: Wed, 8 Oct 2025 10:53:06 +0200
Subject: [PATCH 2/2] Fix dataclasses.field for 3.14
---
crates/ty_ide/src/completion.rs | 7 +-
.../mdtest/dataclasses/dataclasses.md | 49 +++++++++++
.../ty_python_semantic/src/types/call/bind.rs | 85 ++++++++++++-------
3 files changed, 106 insertions(+), 35 deletions(-)
diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs
index 933b940dc4caf..2cec00791e48b 100644
--- a/crates/ty_ide/src/completion.rs
+++ b/crates/ty_ide/src/completion.rs
@@ -1798,12 +1798,11 @@ Meta.
// whether we're in release mode or not. These differences
// aren't really relevant for completion tests AFAIK, so
// just redact them. ---AG
- filters => [(r"(?m)\s*__(annotations|new)__.+$", "")]},
+ filters => [(r"(?m)\s*__(annotations|new|annotate)__.+$", "")]},
{
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: property
mro :: def mro(self) -> list[type]
- __annotate__ :: @Todo(Support for `typing.TypeAlias`) | None
__base__ :: type | None
__bases__ :: tuple[type, ...]
__basicsize__ :: int
@@ -1973,7 +1972,7 @@ Answer.
insta::with_settings!({
// See above: filter out some members which contain @Todo types that are
// rendered differently in release mode.
- filters => [(r"(?m)\s*__(call|reduce_ex)__.+$", "")]},
+ filters => [(r"(?m)\s*__(call|reduce_ex|annotate|signature)__.+$", "")]},
{
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
NO :: Literal[Answer.NO]
@@ -1981,7 +1980,6 @@ Answer.
mro :: bound method .mro() -> list[type]
name :: Any
value :: Any
- __annotate__ :: @Todo(Support for `typing.TypeAlias`) | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
@@ -2024,7 +2022,6 @@ Answer.
__reversed__ :: bound method .__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
__ror__ :: bound method .__ror__(value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
- __signature__ :: @Todo(Support for `typing.TypeAlias`)
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method .__subclasscheck__(subclass: type, /) -> bool
diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
index 01a3baab2e2a5..398939bf9f22e 100644
--- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
+++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
@@ -544,6 +544,55 @@ class A:
y: int
```
+### `kw_only` - Python 3.13
+
+```toml
+[environment]
+python-version = "3.13"
+```
+
+```py
+from dataclasses import dataclass, field
+
+@dataclass
+class Employee:
+ e_id: int = field(kw_only=True, default=0)
+ name: str
+
+Employee("Alice")
+Employee(name="Alice")
+Employee(name="Alice", e_id=1)
+Employee(e_id=1, name="Alice")
+Employee("Alice", e_id=1)
+
+Employee("Alice", 1) # error: [too-many-positional-arguments]
+```
+
+### `kw_only` - Python 3.14
+
+```toml
+[environment]
+python-version = "3.14"
+```
+
+```py
+from dataclasses import dataclass, field
+
+@dataclass
+class Employee:
+ # Python 3.14 introduces a new `doc` parameter for `dataclasses.field`
+ e_id: int = field(kw_only=True, default=0, doc="Global employee ID")
+ name: str
+
+Employee("Alice")
+Employee(name="Alice")
+Employee(name="Alice", e_id=1)
+Employee(e_id=1, name="Alice")
+Employee("Alice", e_id=1)
+
+Employee("Alice", 1) # error: [too-many-positional-arguments]
+```
+
### `slots`
If a dataclass is defined with `slots=True`, the `__slots__` attribute is generated as a tuple. It
diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs
index 60a0394ec8518..fef791d38d9b4 100644
--- a/crates/ty_python_semantic/src/types/call/bind.rs
+++ b/crates/ty_python_semantic/src/types/call/bind.rs
@@ -962,43 +962,46 @@ impl<'db> Bindings<'db> {
}
Some(KnownFunction::Field) => {
- // TODO this will break on Python 3.14 -- we should match by parameter name instead
- if let [default, default_factory, init, .., kw_only] =
- overload.parameter_types()
- {
- let default_ty = match (default, default_factory) {
- (Some(default_ty), _) => *default_ty,
- (_, Some(default_factory_ty)) => default_factory_ty
- .try_call(db, &CallArguments::none())
- .map_or(Type::unknown(), |binding| binding.return_type(db)),
- _ => Type::unknown(),
- };
+ let default =
+ overload.parameter_type_by_name("default").unwrap_or(None);
+ let default_factory = overload
+ .parameter_type_by_name("default_factory")
+ .unwrap_or(None);
+ let init = overload.parameter_type_by_name("init").unwrap_or(None);
+ let kw_only =
+ overload.parameter_type_by_name("kw_only").unwrap_or(None);
+
+ let default_ty = match (default, default_factory) {
+ (Some(default_ty), _) => default_ty,
+ (_, Some(default_factory_ty)) => default_factory_ty
+ .try_call(db, &CallArguments::none())
+ .map_or(Type::unknown(), |binding| binding.return_type(db)),
+ _ => Type::unknown(),
+ };
- let init = init
- .map(|init| !init.bool(db).is_always_false())
- .unwrap_or(true);
+ let init = init
+ .map(|init| !init.bool(db).is_always_false())
+ .unwrap_or(true);
- let kw_only = if Program::get(db).python_version(db)
- >= PythonVersion::PY310
- {
+ let kw_only =
+ if Program::get(db).python_version(db) >= PythonVersion::PY310 {
kw_only.map(|kw_only| !kw_only.bool(db).is_always_false())
} else {
None
};
- // `typeshed` pretends that `dataclasses.field()` returns the type of the
- // default value directly. At runtime, however, this function returns an
- // instance of `dataclasses.Field`. We also model it this way and return
- // a known-instance type with information about the field. The drawback
- // of this approach is that we need to pretend that instances of `Field`
- // are assignable to `T` if the default type of the field is assignable
- // to `T`. Otherwise, we would error on `name: str = field(default="")`.
- overload.set_return_type(Type::KnownInstance(
- KnownInstanceType::Field(FieldInstance::new(
- db, default_ty, init, kw_only,
- )),
- ));
- }
+ // `typeshed` pretends that `dataclasses.field()` returns the type of the
+ // default value directly. At runtime, however, this function returns an
+ // instance of `dataclasses.Field`. We also model it this way and return
+ // a known-instance type with information about the field. The drawback
+ // of this approach is that we need to pretend that instances of `Field`
+ // are assignable to `T` if the default type of the field is assignable
+ // to `T`. Otherwise, we would error on `name: str = field(default="")`.
+ overload.set_return_type(Type::KnownInstance(
+ KnownInstanceType::Field(FieldInstance::new(
+ db, default_ty, init, kw_only,
+ )),
+ ));
}
_ => {
@@ -2782,6 +2785,9 @@ impl<'db> MatchedArgument<'db> {
}
}
+/// Indicates that a parameter of the given name was not found.
+pub(crate) struct UnknownParameterNameError;
+
/// Binding information for one of the overloads of a callable.
#[derive(Debug)]
pub(crate) struct Binding<'db> {
@@ -2919,6 +2925,25 @@ impl<'db> Binding<'db> {
&self.parameter_tys
}
+ /// Returns the bound type for the specified parameter, or `None` if no argument was matched to
+ /// that parameter.
+ ///
+ /// Returns an error if the parameter name is not found.
+ pub(crate) fn parameter_type_by_name(
+ &self,
+ parameter_name: &str,
+ ) -> Result