diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs
index 2cedb435fd..82f34f2a48 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 f9f5580f2b..4c44f523d7 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 248e0f547c..4ecf8f8399 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 8ec32c8c63..f1be98ea46 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 32c3ef02e1..2cec00791e 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, ...]
@@ -1797,7 +1798,7 @@ 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
@@ -1908,6 +1909,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, ...]
@@ -1970,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]
@@ -2020,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__ :: bound method .__signature__() -> str
__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 36709174df..12b76affc1 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/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
index 01a3baab2e..398939bf9f 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 60a0394ec8..fef791d38d 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 init = init
- .map(|init| !init.bool(db).is_always_false())
- .unwrap_or(true);
+ 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 kw_only = if Program::get(db).python_version(db)
- >= PythonVersion::PY310
- {
+ 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 {
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