fix: support string annotations in attrs.converters.optional - #1629
Open
januththedev wants to merge 1 commit into
Open
januththedev wants to merge 1 commit into
januththedev wants to merge 1 commit into
Conversation
januththedev
force-pushed
the
fix/optional-string-annotations
branch
from
September 27, 2026 16:21
b0ddd1d to
62a32ee
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
attrs.converters.optional()crashes on string annotationsDescription
optional()infers the wrapped converter's annotations and then unions them withNone:get_first_param_type()andget_return_type()readinspect.signature(), which returns the annotation as a string when it is a quoted forward reference, or when the converter's own module usesfrom __future__ import annotations(PEP 563)."int" | NoneraisesTypeError: unsupported operand type(s) for |: 'str' and 'NoneType'.typing.Optional["int"]handles strings correctly by wrapping them in aForwardRef.Why this is a bug
optional()'s documented contract is "Type annotations will be inferred from the wrapped converter's, if they have any." A perfectly ordinary, valid converter — one written with quoted forward references, or in a PEP 563 module — makes the construction of the converter crash with aTypeErrorfrom library internals, before it is ever called. The failure has nothing to do with the user's value.It is also a regression: 26.1.0 used
typing.Optional[t], and the current dev cycle replaced it witht | Noneas a Python 3.10+ modernization. The two are only equivalent for real type objects. Ruff's UP045 is what pushes that rewrite, and it does not consider string operands.Reproduction
The change
and the same for the return annotation.
Optional[t] == t | Nonefor real type objects, so every existing code path is unchanged; only the previously-crashing string case is restored. The# noqa: UP045is required because the modernizer would otherwise reintroduce the very rewrite that causes the bug.Tests
TestOptional::test_string_annotationsbuilds a converter with quoted annotations, asserts the wrapped converter's annotations aretyping.Optional[typing.ForwardRef("int")], and checks it still round-trips a value and acceptsNone.TypeError: unsupported operand type(s) for |: 'str' and 'NoneType'atsrc/attr/converters.py:52. After: passes.pytest tests/test_converters.py -q→ 39 passed.pytest tests/test_converters.py tests/test_annotations.py -q→ 87 passed, 2 skipped. The existingtyping.Optional[int]assertions intest_annotations.pystill pass, which is what demonstrates the change is behaviour-preserving for real types.ruff checkandruff format --checkclean.I fetched
src/attr/converters.pyfrommainand it still contains the identicalt | Nonelines, so this is unfixed upstream. I checked the open issues and PRs: #1348 and #831 are both closed and unrelated, and #1619/#1598 concern__init__forward refs.