Skip to content

[ty] fix unary operators on NewTypes of float and complex#22605

Merged
oconnor663 merged 3 commits intomainfrom
newtype_unary
Jan 20, 2026
Merged

[ty] fix unary operators on NewTypes of float and complex#22605
oconnor663 merged 3 commits intomainfrom
newtype_unary

Conversation

@oconnor663
Copy link
Contributor

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 15, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 15, 2026

mypy_primer results

Changes were detected when running on open source projects
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 | _VT@next | _T@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 | _T@next | _VT@next`

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 | int | dict[str, Any] | ... 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 `int | T@resolve_variables | float | ... 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 | int | dict[str, Any] | ... 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 `int | T@resolve_variables | float | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/deployments/runner.py:795:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | ((...) -> Any)`
+ src/prefect/deployments/runner.py:795:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
- 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 | int | dict[str, Any] | ... omitted 4 union elements`
- src/prefect/flow_engine.py:812:32: error[invalid-await] `Unknown | R@FlowRunEngine | Coroutine[Any, Any, R@FlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1401:24: error[invalid-await] `Unknown | R@AsyncFlowRunEngine | Coroutine[Any, Any, R@AsyncFlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1482: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:1490: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:1524: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:1531:25: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
- src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:1750:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- 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 | int | dict[str, Any] | ... 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 | int | dict[str, Any] | ... 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, int | T@resolve_variables | float | ... 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[int | T@resolve_variables | float | ... 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 | int | dict[str, Any] | ... 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 `int | T@resolve_variables | float | ... omitted 4 union elements`
- Found 5411 diagnostics
+ Found 5406 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/index.py:580:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@loc, TVDtype@Index]`, found `InterGetItemLocReduces[Any | Bottom[Series[Any, Any]], TVDtype@Index]`
+ static_frame/core/index.py:580:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@loc, TVDtype@Index]`, found `InterGetItemLocReduces[Bottom[Series[Any, Any]] | Any, TVDtype@Index]`
- static_frame/core/node_selector.py:526:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@InterfaceSelectQuartet, Any]`, found `InterGetItemLocReduces[Unknown | Bottom[Series[Any, Any]], Any]`
+ static_frame/core/node_selector.py:526:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@InterfaceSelectQuartet, Any]`, found `InterGetItemLocReduces[Bottom[Series[Any, Any]] | Unknown, Any]`

No memory usage changes detected ✅

@oconnor663 oconnor663 marked this pull request as draft January 15, 2026 17:46
@oconnor663
Copy link
Contributor Author

oconnor663 commented Jan 15, 2026

Actually this still needs some more work. The following example isn't supported yet (edit: now supported):

from typing import reveal_type, NewType

Foo = NewType('Foo', float)

def f(x: float | Foo):
    print(-x)

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8537aec04d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 11748 to 11749
(_, Type::NewTypeInstance(newtype)) => {
self.infer_unary_expression_type(op, newtype.concrete_base_type(self.db()), unary)

Choose a reason for hiding this comment

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

P2 Badge Preserve NewType self-binding for unary dunders

This new arm always delegates unary ops on NewType instances to concrete_base_type, which changes how Self is bound for dunder methods: -x now binds Self to the base class, while a direct call x.__neg__() still binds Self to the NewType instance via normal member lookup. For a NewType wrapping a class that annotates __neg__/__pos__/__invert__ with typing.Self, this makes -x return the base type instead of the NewType, diverging from x.__neg__() and from the pre-change behavior. Example: class C: def __neg__(self) -> Self: ...; NT = NewType('NT', C); NT(C()).__neg__() remains NT but -NT(C()) would now infer C.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I'm not sure Codex is correct above (need to double check), but it is pointing me to a different issue. This works, because we explicitly added binary operation support:

Foo = NewType("Foo", float)
x = Foo(3.14)
x + x

But this doesn't currently work:

x.__add__(x)  # error[invalid-argument-type]: Argument to bound method `__add__` is incorrect

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, this isn't a NewType issue. It doesn't work with float either on main today (playground):

def _(x: float, y: float):
    # This is fine.
    x + y

    # error[invalid-argument-type]: Argument to bound method `__add__` is incorrect
    x.__add__(y)
    #         ^ Expected `int`, found `int | float`

// `float` and not `int | float`).
Type::NewTypeInstance(new_type_instance) => new_type_instance
.concrete_base_type(db)
.member_lookup_with_policy(db, name, policy),
Copy link
Contributor Author

@oconnor663 oconnor663 Jan 17, 2026

Choose a reason for hiding this comment

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

The previous version of this fix was in infer_unary_expression_type, but that failed to handle more complex cases like:

Foo = NewType("Foo", float)

def _(x: Foo | float):
    -x

This version of the fix here in member_lookup_with_policy handles all cases I'm aware of.

@oconnor663 oconnor663 added ty Multi-file analysis & type inference ecosystem-analyzer labels Jan 17, 2026
@oconnor663 oconnor663 marked this pull request as ready for review January 17, 2026 05:29
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a6b34d80a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 3229 to 3231
Type::NewTypeInstance(new_type_instance) => new_type_instance
.concrete_base_type(db)
.member_lookup_with_policy(db, name, policy),

Choose a reason for hiding this comment

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

P2 Badge Preserve NewType self-binding during member lookup

Delegating Type::NewTypeInstance member lookup to concrete_base_type means the descriptor protocol now binds the instance as the base type instead of the NewType, so methods/properties annotated with typing.Self (or TypeVar bound to Self) will now resolve to the base class. This is a regression for nominal NewType behavior: e.g., with class C: def copy(self) -> Self: ...; NT = NewType("NT", C), NT(C()).copy() will now infer C instead of NT. This only shows up when the base type uses Self-style return typing, but it changes type inference for regular attribute access beyond the float/complex unary operator fix.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a good catch. I added a failing test case in a1c1863 and fixed it in 82cdbfc. In short, I think this special case should only apply to the float and complex unions.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 17, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-return-type 1 4 5
invalid-argument-type 1 3 4
invalid-parameter-default 0 0 7
invalid-assignment 0 0 5
possibly-missing-attribute 0 3 1
unused-ignore-comment 3 0 0
invalid-await 0 2 0
unresolved-attribute 0 0 2
Total 5 12 24

Full report with detailed diff (timing results)

@oconnor663
Copy link
Contributor Author

This should be ready for proper review now :)

Comment on lines +3228 to +3239
// This case needs to come before the `no_instance_fallback` catch-all, so that we
// treat `NewType`s of `float` and `complex` as their special-case union base types.
// Otherwise we'll look up e.g. `__add__` with a `self` type bound to the `NewType`,
// which will fail to match e.g. `float.__add__` (because its `self` parameter is just
// `float` and not `int | float`). However, all other `NewType` cases need to fall
// through, because we generally do want e.g. methods that return `Self` to return the
// `NewType`.
Type::NewTypeInstance(new_type_instance) if new_type_instance.base_is_union(db) => {
new_type_instance
.concrete_base_type(db)
.member_lookup_with_policy(db, name, policy)
}
Copy link
Member

Choose a reason for hiding this comment

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

what would happen if a foo method were added to both int and float in the future1, and it had a Self return type?

NT = NewType(float)

nt = NT(3.14)
reveal_type(nt.foo())  # `int | float` or `NT`?

Footnotes

  1. I'll get writing the "add a foo method to float and int" PEP ASAP

Copy link
Member

Choose a reason for hiding this comment

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

(currently the only method that exists on both classes and returns Self is __new__ -- float.fromhex() returns Self, but it only exists on the float class, not the int class)

Copy link
Member

Choose a reason for hiding this comment

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

Uff, this does seem like a very silly and unlikely edge case I'm pointing out, though. And I'm not even sure what the correct answer there even is. I think you can just ignore me here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I went looking for Self return types in typeshed, and it looked like they were all classmethods today, so I figured maybe I didn't need a clear answer to this 😅 (Similarly, probably obvious to you, but for completeness / my notes: if I subclass int or float, and then NewType that, the base will be NewTypeBase::ClassLiteral, and this special case won't apply.)

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.

it makes me a bit sad how many edge cases NewTypes of float are causing (and how many carve-outs we have to add as a result), but I don't see a way around it :( this LGTM!

@oconnor663
Copy link
Contributor Author

Yeah it also makes me nervous. Like how many other things (now or in the future) will "actually be unions on the inside" in some sense, and will need to go through a similarly painful process of accumulating all these special cases?

@oconnor663 oconnor663 merged commit f5c4adf into main Jan 20, 2026
49 checks passed
@oconnor663 oconnor663 deleted the newtype_unary branch January 20, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

*Unary* operators not correctly inherited with NewType(..., float)

2 participants