From e646bd917e38eaab5882170aa563700d910dee03 Mon Sep 17 00:00:00 2001 From: Finite State Machine <38001514+finite-state-machine@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:50:56 -0500 Subject: [PATCH 1/9] Fix `optional` validator to accept tuples of len > 1 --- src/attr/validators.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 36a7e800c..18fb112c8 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -54,7 +54,7 @@ def optional( validator: ( _ValidatorType[_T] | list[_ValidatorType[_T]] - | tuple[_ValidatorType[_T]] + | tuple[_ValidatorType[_T], ...] ), ) -> _ValidatorType[_T | None]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... From 52cd1d7bc3120ba0e53d9c8119c1f345f2c57f7a Mon Sep 17 00:00:00 2001 From: Finite State Machine Date: Sat, 3 Jan 2026 19:19:24 -0500 Subject: [PATCH 2/9] Add changelog --- changelog.d/1496.change.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1496.change.md diff --git a/changelog.d/1496.change.md b/changelog.d/1496.change.md new file mode 100644 index 000000000..a3dd5a305 --- /dev/null +++ b/changelog.d/1496.change.md @@ -0,0 +1 @@ +Fix type annotations for `optional` validator so it no longer rejects tuples of len > 1 From 110c6d26b140880d5377e49d8a60a4a2e3867a9c Mon Sep 17 00:00:00 2001 From: Finite State Machine Date: Sat, 3 Jan 2026 19:19:35 -0500 Subject: [PATCH 3/9] Add `optional` validator over 2-tuple test case to `baseline.py` --- typing-examples/baseline.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/typing-examples/baseline.py b/typing-examples/baseline.py index 195b3262b..a34e20d97 100644 --- a/typing-examples/baseline.py +++ b/typing-examples/baseline.py @@ -94,6 +94,15 @@ class Validated: num: int = attrs.field(validator=attrs.validators.ge(0)) +@attrs.define +class ValidatedOptionalOverTuple: + num: int | None = attrs.field( + validator=attrs.validators.optional( + (attrs.validators.instance_of(int), attrs.validators.ge(0)) + ) + ) + + @attrs.define class ValidatedInconsistentOr: num: int | str = attrs.field( From 61a1dd0ee474e9c8cfbe055ebb463d8168c65bbe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:20:34 +0000 Subject: [PATCH 4/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- typing-examples/baseline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typing-examples/baseline.py b/typing-examples/baseline.py index a34e20d97..15542cab7 100644 --- a/typing-examples/baseline.py +++ b/typing-examples/baseline.py @@ -97,10 +97,10 @@ class Validated: @attrs.define class ValidatedOptionalOverTuple: num: int | None = attrs.field( - validator=attrs.validators.optional( - (attrs.validators.instance_of(int), attrs.validators.ge(0)) - ) - ) + validator=attrs.validators.optional( + (attrs.validators.instance_of(int), attrs.validators.ge(0)) + ) + ) @attrs.define From c1129cc578d0742c57bca719189e95384d5313a6 Mon Sep 17 00:00:00 2001 From: Finite State Machine Date: Sat, 3 Jan 2026 19:38:37 -0500 Subject: [PATCH 5/9] Work around `ty` false positive `invalid-argument-type` --- typing-examples/baseline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/typing-examples/baseline.py b/typing-examples/baseline.py index 15542cab7..3f712c763 100644 --- a/typing-examples/baseline.py +++ b/typing-examples/baseline.py @@ -98,7 +98,11 @@ class Validated: class ValidatedOptionalOverTuple: num: int | None = attrs.field( validator=attrs.validators.optional( - (attrs.validators.instance_of(int), attrs.validators.ge(0)) + (attrs.validators.instance_of(int), attrs.validators.ge(int(0))) + # `int(0)` needed to avoid false positive + # `invalid-argument-type` when checking with + # ty 0.0.8 (aa7559db8 2025-12-29): ty assumes `ge`'s + # specialization is `Literal[0]` rather than `int` ) ) From 5b413827365559bac9cb7b72e0b75a0ce4328772 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:42:54 +0000 Subject: [PATCH 6/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- typing-examples/baseline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typing-examples/baseline.py b/typing-examples/baseline.py index 3f712c763..4a666dd8e 100644 --- a/typing-examples/baseline.py +++ b/typing-examples/baseline.py @@ -98,7 +98,7 @@ class Validated: class ValidatedOptionalOverTuple: num: int | None = attrs.field( validator=attrs.validators.optional( - (attrs.validators.instance_of(int), attrs.validators.ge(int(0))) + (attrs.validators.instance_of(int), attrs.validators.ge(0)) # `int(0)` needed to avoid false positive # `invalid-argument-type` when checking with # ty 0.0.8 (aa7559db8 2025-12-29): ty assumes `ge`'s From 849dbeecfa38c22dd774bf8642edfcff37e6e25f Mon Sep 17 00:00:00 2001 From: Finite State Machine Date: Sat, 3 Jan 2026 20:03:49 -0500 Subject: [PATCH 7/9] Revert "fixes" from pre-commit.com hooks [skip pre-commit.ci] This reverts commit 5b413827365559bac9cb7b72e0b75a0ce4328772. --- typing-examples/baseline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typing-examples/baseline.py b/typing-examples/baseline.py index 4a666dd8e..3f712c763 100644 --- a/typing-examples/baseline.py +++ b/typing-examples/baseline.py @@ -98,7 +98,7 @@ class Validated: class ValidatedOptionalOverTuple: num: int | None = attrs.field( validator=attrs.validators.optional( - (attrs.validators.instance_of(int), attrs.validators.ge(0)) + (attrs.validators.instance_of(int), attrs.validators.ge(int(0))) # `int(0)` needed to avoid false positive # `invalid-argument-type` when checking with # ty 0.0.8 (aa7559db8 2025-12-29): ty assumes `ge`'s From 3e55d900046edb09dce22cd93d1ef6bb73cdbdfa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 01:04:43 +0000 Subject: [PATCH 8/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- typing-examples/baseline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typing-examples/baseline.py b/typing-examples/baseline.py index 3f712c763..4a666dd8e 100644 --- a/typing-examples/baseline.py +++ b/typing-examples/baseline.py @@ -98,7 +98,7 @@ class Validated: class ValidatedOptionalOverTuple: num: int | None = attrs.field( validator=attrs.validators.optional( - (attrs.validators.instance_of(int), attrs.validators.ge(int(0))) + (attrs.validators.instance_of(int), attrs.validators.ge(0)) # `int(0)` needed to avoid false positive # `invalid-argument-type` when checking with # ty 0.0.8 (aa7559db8 2025-12-29): ty assumes `ge`'s From e0a6b93efdc58d8b959c172de66b4c67f64cb629 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 14 Mar 2026 14:01:54 +0100 Subject: [PATCH 9/9] Silence ty ref https://github.com/astral-sh/ty/issues/3016 --- changelog.d/1496.change.md | 2 +- typing-examples/baseline.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/changelog.d/1496.change.md b/changelog.d/1496.change.md index a3dd5a305..8faaabaaa 100644 --- a/changelog.d/1496.change.md +++ b/changelog.d/1496.change.md @@ -1 +1 @@ -Fix type annotations for `optional` validator so it no longer rejects tuples of len > 1 +Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator. diff --git a/typing-examples/baseline.py b/typing-examples/baseline.py index 4a666dd8e..573ca99a0 100644 --- a/typing-examples/baseline.py +++ b/typing-examples/baseline.py @@ -98,11 +98,7 @@ class Validated: class ValidatedOptionalOverTuple: num: int | None = attrs.field( validator=attrs.validators.optional( - (attrs.validators.instance_of(int), attrs.validators.ge(0)) - # `int(0)` needed to avoid false positive - # `invalid-argument-type` when checking with - # ty 0.0.8 (aa7559db8 2025-12-29): ty assumes `ge`'s - # specialization is `Literal[0]` rather than `int` + (attrs.validators.instance_of(int), attrs.validators.ge(0)) # ty:ignore [invalid-argument-type] ) )