Skip to content

[ty] Infer ParamSpec from class constructors for callable protocols#22853

Merged
charliermarsh merged 1 commit intomainfrom
charlie/callable
Feb 2, 2026
Merged

[ty] Infer ParamSpec from class constructors for callable protocols#22853
charliermarsh merged 1 commit intomainfrom
charlie/callable

Conversation

@charliermarsh
Copy link
Member

Summary

Closes astral-sh/ty#2587.

@charliermarsh charliermarsh added the ty Multi-file analysis & type inference label Jan 25, 2026
@charliermarsh charliermarsh marked this pull request as ready for review January 25, 2026 19:34
@charliermarsh charliermarsh marked this pull request as draft January 25, 2026 19:35
@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 25, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 25, 2026

mypy_primer results

Changes were detected when running on open source projects
pydantic (https://github.com/pydantic/pydantic)
- pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | ((dict[str, int | float | str | ... omitted 3 union elements], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
+ pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | ((dict[str, Divergent], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
- pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
+ pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
- pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
+ pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, Divergent], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`

psycopg (https://github.com/psycopg/psycopg)
- psycopg/psycopg/_typeinfo.py:102:51: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `RowFactory[tuple[Any, ...]]`, found `def dict_row(cursor: BaseCursor[Any, Any]) -> RowMaker[dict[str, Any]]`
- psycopg/psycopg/_typeinfo.py:110:40: error[invalid-argument-type] Argument to bound method `_from_records` is incorrect: Expected `Sequence[dict[str, Any]]`, found `list[tuple[Any, ...]]`
- psycopg/psycopg/_typeinfo.py:120:46: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `AsyncRowFactory[tuple[Any, ...]]`, found `def dict_row(cursor: BaseCursor[Any, Any]) -> RowMaker[dict[str, Any]]`
- psycopg/psycopg/_typeinfo.py:128:40: error[invalid-argument-type] Argument to bound method `_from_records` is incorrect: Expected `Sequence[dict[str, Any]]`, found `list[tuple[Any, ...]]`
- Found 653 diagnostics
+ Found 649 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/prefect/deployments/runner.py:997:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
+ src/prefect/deployments/runner.py:997:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | ((...) -> Any)`
+ src/prefect/flow_engine.py:989:32: error[invalid-await] `Unknown | R@FlowRunEngine | Coroutine[Any, Any, R@FlowRunEngine]` is not awaitable
+ src/prefect/flow_engine.py:1580:24: error[invalid-await] `Unknown | R@AsyncFlowRunEngine | Coroutine[Any, Any, R@AsyncFlowRunEngine]` is not awaitable
+ src/prefect/flow_engine.py:1661:43: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Unknown | R@run_generator_flow_sync`
+ src/prefect/flow_engine.py:1669:21: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_sync`
+ src/prefect/flow_engine.py:1703:44: warning[possibly-missing-attribute] Attribute `__anext__` may be missing on object of type `Unknown | R@run_generator_flow_async`
+ src/prefect/flow_engine.py:1710:25: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flows.py:285:34: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:285:34: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
- src/prefect/flows.py:403:68: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:403:68: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
- src/prefect/flows.py:1877:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ src/prefect/flows.py:1937:21: error[no-matching-overload] No overload of function `run_coro_as_sync` matches arguments
- Found 5359 diagnostics
+ Found 5365 diagnostics

hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/models/sw1d_network.py:1168:75: error[invalid-argument-type] Argument to bound method `append_submodel` is incorrect: Expected `RoutingModel_V1 | RoutingModel_V2`, found `RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3`
+ hydpy/models/sw1d_network.py:1169:78: error[invalid-argument-type] Argument to bound method `append_submodel` is incorrect: Expected `RoutingModel_V2 | RoutingModel_V3`, found `RoutingModel_V1 | RoutingModel_V2 | RoutingModel_V3`
- Found 660 diagnostics
+ Found 662 diagnostics

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/chain/decoding/tools.py:96:44: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- rotkehlchen/chain/decoding/tools.py:97:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress`, found `A@BaseDecoderTools`
+ rotkehlchen/chain/decoding/tools.py:99:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `Sequence[A@BaseDecoderTools]`, found `Unknown | tuple[BTCAddress, ...] | tuple[ChecksumAddress, ...] | tuple[SubstrateAddress, ...] | tuple[SolanaAddress, ...]`
- rotkehlchen/chain/decoding/tools.py:98:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress | None`, found `A@BaseDecoderTools | None`
+ rotkehlchen/chain/decoding/tools.py:100:62: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 2050 diagnostics
+ Found 2051 diagnostics

core (https://github.com/home-assistant/core)
- homeassistant/util/variance.py:47:12: error[invalid-return-type] Return type does not match returned value: expected `(**_P@ignore_variance) -> _R@ignore_variance`, found `_Wrapped[_P@ignore_variance, int | _R@ignore_variance | float | datetime, _P@ignore_variance, _R@ignore_variance | int | float | datetime]`
- Found 14481 diagnostics
+ Found 14480 diagnostics

No memory usage changes detected ✅

@charliermarsh
Copy link
Member Author

Unless I'm mistaken, I think the second comment (astral-sh/ty#2587 (comment)) is a separate issue.

@charliermarsh
Copy link
Member Author

I believe the psycopg change is correctly removing false positives.

@charliermarsh charliermarsh marked this pull request as ready for review January 25, 2026 20:03
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit hesitant to make too many changes to how generic protocols work while #21902 is still in progress (it should land this week, hopefully). Having said that, the tests you've added don't appear to be fixed by Doug's branch there, and it's true that __call__ members on protocols need to be quite heavily special-cased (sadly)

Comment on lines +973 to +974
# error: [invalid-argument-type]
create(MyType, "wrong type")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# error: [invalid-argument-type]
create(MyType, "wrong type")
# error: [too-many-positional-arguments]
create(MyType, 1, 2)
# error: [missing-argument]
# error: [unknown-argument]
create(MyType, y=2)
# error: [unknown-argument]
create(MyType, x=1, y=2)
# error: [invalid-argument-type]
create(MyType, "wrong type")

class NoArgs(ParentClass):
pass

create(NoArgs) # OK - P is inferred as []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
create(NoArgs) # OK - P is inferred as []
create(NoArgs) # OK - P is inferred as []
create(NoArgs, 1) # error: [too-many-positional-arguments]

@AlexWaygood
Copy link
Member

(I'd like @dcreager's review on this one!)

@carljm carljm removed their request for review January 27, 2026 01:27
Copy link
Member

@dcreager dcreager left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit hesitant to make too many changes to how generic protocols work while #21902 is still in progress (it should land this week, hopefully). Having said that, the tests you've added don't appear to be fixed by Doug's branch there, and it's true that __call__ members on protocols need to be quite heavily special-cased (sadly)

👍 Code looks good! This does not look to conflict much with #21902.

}

/// Infer type mappings by comparing formal callable signatures against actual callables.
fn infer_from_callable_signature(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to this refactoring

@sharkdp sharkdp removed their request for review February 2, 2026 15:29
@charliermarsh charliermarsh merged commit 8d78aa6 into main Feb 2, 2026
49 checks passed
@charliermarsh charliermarsh deleted the charlie/callable branch February 2, 2026 15:55
carljm added a commit that referenced this pull request Feb 2, 2026
* main: (48 commits)
  add info for non_octal permissions (#22972)
  Fix empty body rule rendering (#23039)
  [ty] Infer `ParamSpec` from class constructors for callable protocols (#22853)
  Update NPM Development dependencies (#23030)
  Update CodSpeedHQ/action action to v4.8.2 (#23029)
  [ty] remove special handling for `Any()` in match class patterns (#23011)
  Update Rust crate get-size2 to v0.7.4 (#23022)
  Update Rust crate insta to v1.46.1 (#23023)
  Update taiki-e/install-action action to v2.67.11 (#23033)
  Update Rust crate colored to v3.1.1 (#23031)
  Update cargo-bins/cargo-binstall action to v1.17.3 (#23028)
  Update Rust crate uuid to v1.20.0 (#23032)
  [ty] Avoid using `.node()` for detecting `Self` (#23000)
  Update Rust crate proc-macro2 to v1.0.106 (#23024)
  Update actions/setup-python action to v6.2.0 (#23027)
  [ty] fix query cycles in decorated function with parameter defaults (#23014)
  Update Rust crate quote to v1.0.44 (#23025)
  Update Rust crate thiserror to v2.0.18 (#23026)
  Update Rust crate filetime to v0.2.27 (#23021)
  Update Rust crate clearscreen to v4.0.3 (#23020)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False negative when inferring paramspec for a class constructor

3 participants