Skip to content

[ty] Validate constructor arguments when a class is used as a decorator#22377

Merged
charliermarsh merged 4 commits intomainfrom
charlie/class-decorator-validate-args
Jan 17, 2026
Merged

[ty] Validate constructor arguments when a class is used as a decorator#22377
charliermarsh merged 4 commits intomainfrom
charlie/class-decorator-validate-args

Conversation

@charliermarsh
Copy link
Member

Summary

If a class is used as a decorator, we now use the class constructor.

Closes astral-sh/ty#2232.

@charliermarsh charliermarsh added the ty Multi-file analysis & type inference label Jan 4, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 4, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 4, 2026

mypy_primer results

Changes were detected when running on open source projects
werkzeug (https://github.com/pallets/werkzeug)
- src/werkzeug/debug/__init__.py:553:73: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ src/werkzeug/debug/tbtools.py:266:16: error[invalid-return-type] Return type does not match returned value: expected `list[DebugFrameSummary]`, found `list[FrameSummary | Unknown]`
+ tests/test_formparser.py:222:50: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Argument type `FileStorage` does not satisfy upper bound `_WrappedBuffer` of type variable `_BufferT_co`
+ tests/test_formparser.py:222:50: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `_WrappedBuffer`, found `FileStorage`
+ tests/test_routing.py:1310:9: warning[possibly-missing-attribute] Attribute `endpoint` may be missing on object of type `Rule | None`
+ tests/test_test.py:306:12: warning[possibly-missing-attribute] Attribute `username` may be missing on object of type `Authorization | None`
+ tests/test_test.py:307:12: warning[possibly-missing-attribute] Attribute `password` may be missing on object of type `Authorization | None`
+ tests/test_utils.py:285:5: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `(Any, /) -> Unknown`, found `def foo() -> Unknown`
+ tests/test_wrappers.py:179:12: warning[possibly-missing-attribute] Attribute `type` may be missing on object of type `Authorization | None`
+ tests/test_wrappers.py:180:12: warning[possibly-missing-attribute] Attribute `username` may be missing on object of type `Authorization | None`
+ tests/test_wrappers.py:181:12: warning[possibly-missing-attribute] Attribute `password` may be missing on object of type `Authorization | None`
+ tests/test_wrappers.py:189:12: warning[possibly-missing-attribute] Attribute `type` may be missing on object of type `Authorization | None`
+ tests/test_wrappers.py:190:12: warning[possibly-missing-attribute] Attribute `username` may be missing on object of type `Authorization | None`
+ tests/test_wrappers.py:191:12: warning[possibly-missing-attribute] Attribute `password` may be missing on object of type `Authorization | None`
+ tests/test_wrappers.py:243:27: error[invalid-argument-type] Argument to bound method `write` is incorrect: Expected `bytes`, found `Literal["bar"]`
+ tests/test_wrappers.py:465:5: error[invalid-assignment] Invalid assignment to data descriptor attribute `stream` on type `Request` with custom `__set__` method
+ tests/test_wrappers.py:710:27: error[invalid-argument-type] Argument to bound method `write` is incorrect: Expected `bytes`, found `Literal["Hello "]`
+ tests/test_wrappers.py:711:27: error[invalid-argument-type] Argument to bound method `write` is incorrect: Expected `bytes`, found `Literal["World!"]`
+ tests/test_wrappers.py:1079:12: warning[possibly-missing-attribute] Attribute `ranges` may be missing on object of type `Range | None`
+ tests/test_wrappers.py:1082:26: warning[possibly-missing-attribute] Attribute `make_content_range` may be missing on object of type `Range | None`
+ tests/test_wrappers.py:1124:28: error[invalid-argument-type] Argument to bound method `writelines` is incorrect: Expected `Iterable[bytes]`, found `list[str]`
+ tests/test_wrappers.py:1129:28: error[invalid-argument-type] Argument to bound method `writelines` is incorrect: Expected `Iterable[bytes]`, found `list[str]`
+ tests/test_wrappers.py:1133:28: error[invalid-argument-type] Argument to bound method `writelines` is incorrect: Expected `Iterable[bytes]`, found `list[str]`
- Found 386 diagnostics
+ Found 407 diagnostics

tornado (https://github.com/tornadoweb/tornado)
- tornado/gen.py:255:62: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `None | Awaitable[Unknown] | list[Awaitable[Unknown]] | dict[Any, Awaitable[Unknown]] | Future[Unknown]`, found `_T@next | _T@next | _VT@next`
+ tornado/gen.py:255:62: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `None | Awaitable[Unknown] | list[Awaitable[Unknown]] | dict[Any, Awaitable[Unknown]] | Future[Unknown]`, found `_T@next | _VT@next | _T@next`

discord.py (https://github.com/Rapptz/discord.py)
- discord/ext/commands/context.py:486:63: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/context.py:520:54: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ discord/ext/commands/converter.py:379:16: error[invalid-return-type] Return type does not match returned value: expected `tuple[int | None, int, int]`, found `tuple[(Guild & ~AlwaysTruthy) | None | int, int, int]`
- discord/ext/commands/converter.py:1032:82: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/converter.py:1036:85: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/converter.py:1051:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/core.py:2067:47: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/core.py:2069:68: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/core.py:2278:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/core.py:2304:49: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- discord/ext/commands/core.py:2429:40: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 551 diagnostics
+ Found 542 diagnostics

speedrun.com_global_scoreboard_webapp (https://github.com/Avasam/speedrun.com_global_scoreboard_webapp)
+ backend/api/global_scoreboard_api.py:104:32: warning[redundant-cast] Value is already of type `str`
- Found 20 diagnostics
+ Found 21 diagnostics

mkdocs (https://github.com/mkdocs/mkdocs)
+ mkdocs/tests/structure/page_tests.py:539:14: error[no-matching-overload] No overload of function `open` matches arguments
- Found 220 diagnostics
+ Found 221 diagnostics

strawberry (https://github.com/strawberry-graphql/strawberry)
- strawberry/federation/schema.py:301:62: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- strawberry/federation/schema.py:322:62: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- strawberry/types/field.py:200:25: error[not-iterable] Object of type `object` is not iterable
- Found 348 diagnostics
+ Found 345 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | str | int | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables | str | int | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any]` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[T@resolve_block_document_references | dict[str, Any] | Unknown]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[T@resolve_block_document_references | dict[str, Any] | str | ... omitted 5 union elements]`
- src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, T@resolve_variables | Unknown]`
+ src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, T@resolve_variables | str | int | ... omitted 5 union elements]`
- src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[T@resolve_variables | Unknown]`
+ src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[T@resolve_variables | str | int | ... omitted 5 union elements]`
- src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
+ src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
- src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables`
+ src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables | str | int | ... omitted 4 union elements`

dd-trace-py (https://github.com/DataDog/dd-trace-py)
+ ddtrace/internal/symbol_db/symbols.py:362:22: warning[redundant-cast] Value is already of type `Scope | None`
+ tests/internal/symbol_db/test_symbols.py:171:20: warning[possibly-missing-attribute] Attribute `scopes` may be missing on object of type `Scope | None`
- Found 8413 diagnostics
+ Found 8415 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 46 diagnostics
+ Found 47 diagnostics

aiohttp (https://github.com/aio-libs/aiohttp)
+ aiohttp/cookiejar.py:342:23: error[no-matching-overload] No overload of function `__new__` matches arguments
- Found 180 diagnostics
+ Found 181 diagnostics

hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/core/hydpytools.py:2631:13: error[invalid-assignment] Object of type `tuple[_Node@topological_sort, ...]` is not assignable to attribute `_deviceorder` of type `tuple[Node | Element, ...] | None`
+ hydpy/core/selectiontools.py:829:25: error[invalid-argument-type] Argument to bound method `add` is incorrect: Expected `_Node@ancestors`, found `Node | Element`
+ hydpy/core/selectiontools.py:981:25: error[invalid-argument-type] Argument to bound method `add` is incorrect: Expected `_Node@descendants`, found `Node | Element`
+ hydpy/core/threadingtools.py:247:66: error[invalid-argument-type] Argument to bound method `__call__` is incorrect: Expected `_Node@dfs_successors | None`, found `Element`
+ hydpy/core/threadingtools.py:259:70: error[invalid-argument-type] Argument to bound method `__call__` is incorrect: Expected `_Node@dfs_successors | None`, found `Node`
- Found 664 diagnostics
+ Found 669 diagnostics

django-stubs (https://github.com/typeddjango/django-stubs)
- django-stubs/db/models/fields/__init__.pyi:150:34: error[unresolved-attribute] Object of type `cached_property[Unknown]` has no attribute `_ValidatorCallable`
+ django-stubs/db/models/fields/__init__.pyi:150:34: error[unresolved-attribute] Object of type `cached_property[list[Unknown]]` has no attribute `_ValidatorCallable`
- django-stubs/db/models/fields/__init__.pyi:178:30: error[unresolved-attribute] Object of type `cached_property[Unknown]` has no attribute `_ValidatorCallable`
+ django-stubs/db/models/fields/__init__.pyi:178:30: error[unresolved-attribute] Object of type `cached_property[list[Unknown]]` has no attribute `_ValidatorCallable`
- django-stubs/db/models/fields/__init__.pyi:216:34: error[unresolved-attribute] Object of type `cached_property[Unknown]` has no attribute `_ValidatorCallable`
+ django-stubs/db/models/fields/__init__.pyi:216:34: error[unresolved-attribute] Object of type `cached_property[list[Unknown]]` has no attribute `_ValidatorCallable`
- tests/assert_type/apps/test_config.py:37:1: error[type-assertion-failure] Type `str` does not match asserted type `Unknown`
- Found 448 diagnostics
+ Found 447 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/bus.py:671:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[Bus[Any], object_]`, found `InterGetItemLocReduces[Bus[Any] | Bottom[Index[Any]] | Bottom[Series[Any, Any]] | ... omitted 6 union elements, object_]`
+ static_frame/core/bus.py:671:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[Bus[Any], object_]`, found `InterGetItemLocReduces[Bus[Any] | Bottom[Series[Any, Any]] | ndarray[Never, Never] | ... omitted 6 union elements, object_]`
- static_frame/core/bus.py:675:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Bus[Any], object_]`, found `InterGetItemILocReduces[Bus[Any] | Bottom[Index[Any]] | TypeBlocks | ... omitted 6 union elements, object_ | Self@iloc]`
+ static_frame/core/bus.py:675:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Bus[Any], object_]`, found `InterGetItemILocReduces[Bus[Any] | ndarray[Never, Never] | TypeBlocks | ... omitted 6 union elements, object_ | Self@iloc]`

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: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:100:62: 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:98:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress | None`, found `A@BaseDecoderTools | None`
- Found 2057 diagnostics
+ Found 2056 diagnostics

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
- tests/frame/test_groupby.py:229:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- tests/frame/test_groupby.py:625:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- Found 4353 diagnostics
+ Found 4351 diagnostics

No memory usage changes detected ✅

@charliermarsh charliermarsh marked this pull request as ready for review January 4, 2026 23:28
@AlexWaygood
Copy link
Member

this looks like it might conflict quite badly with #22124, though I have no idea how close that PR is to landing

@charliermarsh charliermarsh force-pushed the charlie/class-decorator-validate-args branch from aa17527 to 10d4e56 Compare January 6, 2026 19:44
@Hugo-Polloli
Copy link
Contributor

this looks like it might conflict quite badly with #22124, though I have no idea how close that PR is to landing

#22124 is currently ready for review, but with a few questions in need of some guidance, so I would not say it's going to land very soon
If this one ends up being merged before #22124, I'll do the necessary work to rebase and rework the PR so that we don't regress (haven't had the time to check how bad the conflict is yet)

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.

This isn't a full review but it looks like I got halfway through reviewing last week, so here's a partial review

@AlexWaygood AlexWaygood changed the title [ty] Validate class decorator constructor arguments [ty] Validate constructor arguments when a class is used as a decorator Jan 16, 2026
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 did not review the tests very thoroughly since @AlexWaygood already did.

On the code side, I have a mild concern that we're duplicating the "class constructors should not use try_call" logic in so many places, but that fact isn't introduced by this PR, so I don't consider that a blocker here. If we don't have an open issue for that (making try_call work for all call sites and callables) we should open one.

Comment on lines 7255 to 7258
Err(err) => {
err.report_diagnostic(&self.context, decorator_ty, decorator_node.into());
err.return_type()
}
Copy link
Member

Choose a reason for hiding this comment

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

This error arm is slightly different than the one below. Do they produce different diagnostics? If so, is that on purpose? If we could consolidate these two arms to be identical, then we could collapse this down overall to something like

let call_arguments = CallArguments::positional([decorated_ty]);
let call_result = if use_constructor_call {
    decorator_ty.try_call_constructor(
        self.db(),
        |_| call_arguments,
        TypeContext::default(),
    )
} else {
    decorator_ty
        .try_call(self.db(), &call_arguments)
        .map(|bindings| bindings.return_type(self.db()))
};
match call_result {
    Ok(return_ty) => ...
    Err(err) => ...
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the error types are slightly different -- ConstructorCallError vs. CallError -- but I tried to unify more.

@charliermarsh charliermarsh force-pushed the charlie/class-decorator-validate-args branch 2 times, most recently from a304a6f to e527848 Compare January 17, 2026 15:05
@charliermarsh charliermarsh force-pushed the charlie/class-decorator-validate-args branch from e527848 to 9408474 Compare January 17, 2026 15:17
@charliermarsh
Copy link
Member Author

(Reviewing ecosystem results...)

@charliermarsh
Copy link
Member Author

I believe the WerkZeug diagnostics are true positives because we now "see" the cached property decorator.

@charliermarsh charliermarsh merged commit df58d67 into main Jan 17, 2026
49 checks passed
@charliermarsh charliermarsh deleted the charlie/class-decorator-validate-args branch January 17, 2026 15:49
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.

No argument errors reported when a class is used as a decorator

4 participants