KEMBAR78
Add `typing._UnionSpecialForm` and add it to `isinstance`/`issubclass` by KotlinIsland · Pull Request #7508 · python/typeshed · GitHub
Skip to content

Conversation

@KotlinIsland
Copy link
Contributor

@KotlinIsland KotlinIsland commented Mar 17, 2022

isinstance is currently not accepting instances of _UnionGenericAlias aka typing.Union.

This change introduces a sussy fake impostor type used as a marker for these values.

Resolves #7505

@AlexWaygood
Copy link
Member

Where is the ci?

It's been sleepy lately; looks like it's woken up now though

@KotlinIsland KotlinIsland changed the title Add typing.UnionSpecialForm and add it to isinstance/issubclass Add typing._UnionSpecialForm and add it to isinstance/issubclass Mar 17, 2022
@github-actions

This comment has been minimized.

@AlexWaygood
Copy link
Member

@KotlinIsland, have you tested locally to check that this solves the issue?

@KotlinIsland
Copy link
Contributor Author

Not at all. Whats the quickest way to do this?

@github-actions
Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

steam.py (https://github.com/Gobot1234/steam.py)
- steam/ext/commands/converters.py:507: error: Incompatible types in assignment (expression has type "Union[Any, object]", variable has type "Union[T, str, Tuple[T, ...], Tuple[str, ...], Union[ConverterBase[Any], BasicConverter[Any]]]")  [assignment]
+ steam/ext/commands/converters.py:507: error: Incompatible types in assignment (expression has type "Union[Any, T, str, _UnionSpecialForm]", variable has type "Union[T, str, Tuple[T, ...], Tuple[str, ...], Union[ConverterBase[Any], BasicConverter[Any]]]")  [assignment]

@AlexWaygood
Copy link
Member

AlexWaygood commented Mar 17, 2022

Not at all. Whats the quickest way to do this?

Run mypy test_script.py --custom-typeshed-dir path_to_typeshed_with_this_patch_applied

test_script.py can just contain the snippet you supplied in #7505 (comment). If mypy doesn't report any errors, it works!

@KotlinIsland
Copy link
Contributor Author

Okay, just tested it and it works fine in the negative and positive.

@AlexWaygood
Copy link
Member

AlexWaygood commented Mar 18, 2022

The primer output indicates that mypy has no problems with this. I'm actually a little surprised at how blasé the CI is about this change.

@erictraut / @rchen152: would this change break pyright or pytype at all?

@erictraut
Copy link
Contributor

It might break pyright (I'd have to confirm), but it would be an easy fix to make. T

his particular fix doesn't look correct to me though. This appears to be a bug in mypy, not in typeshed. Shouldn't mypy just fix its handling here?

I'll also point out that I don't understand why someone would want to use the old Union form (pre PEP 604) in an isinstance call if their code is running on Python 3.10. If their code is meant to run on a version of Python older than that, then the use of Union will generate a runtime error.

So this change seems both unnecessary and in the wrong place.

@JelleZijlstra
Copy link
Member

I'll also point out that I don't understand why someone would want to use the old Union form (pre PEP 604) in an isinstance call if their code is running on Python 3.10.

I can't find a realistic use case either. Some uses of the | operator do produce _UnionGenericAlias instead of types.UnionType:

>>> type(int | list[int])
<class 'types.UnionType'>
>>> type(int | List[int])
<class 'typing._UnionGenericAlias'>

But isinstance() doesn't work with List[int].

This works (which surprised me the other day):

>>> isinstance(1, Union[int, str])
True

But as Eric said, why would you do that if you can use int | str instead? In 3.9, where the | operator doesn't exist yet, isinstance() on Union doesn't work either.

@KotlinIsland
Copy link
Contributor Author

KotlinIsland commented Mar 18, 2022

Because it reflects the runtime behavior. How is that not reason enough?

This appears to be a bug in mypy

@erictraut Could you please elaborate on this, I can't see any way that this could possibly be a bug in mypy.

I'll also point out that I don't understand why someone would want to use the old Union form (pre PEP 604) in an isinstance call

It's situations like this that make me so confused at the decisions made in Python, if there is no use case for this functionality, then why was it ever added to the implementation of isinstance in the first place?

@erictraut
Copy link
Contributor

typing.Union is a special form that needs to be special-cased by type checkers. Mypy special-cases it in most cases where it's appropriate to do so, but it apparently doesn't have the appropriate special-casing in place for isinstance. This is understandable given that support for Union was added only recently to the runtime. The same code type checks fine in pyright.

Think about all of the other places in the type system where unions are accepted. Typeshed stubs don't need to specify "a union can be used here" because type checkers automatically handle unions as a special case.

if there is no use case for this functionality, then why was it ever added to the implementation of isinstance in the first place?

This was added in Python 3.10 to support the new syntax in PEP 604. At runtime, x | y is converted into a UnionType. So if the runtime wants to support isinstance(a, x | y), it needs to handle UnionType arguments for the second parameter. This means the runtime also happens to support isinstance(a, Union[x, y]) in Python 3.10 and newer, but that wasn't the intent or motivation for the change. The fact that it happens to support this is incidental. As I mentioned above, I don't know why anyone would use the old form of Union with isinstance. It's not compatible with Python <3.10 and there's a better syntax for Python >=3.10. If your code needs to be compatible with older versions of Python, you should use isinstance(a, (x, y)) instead.

@AlexWaygood
Copy link
Member

Because it reflects the runtime behavior. How is that not reason enough?

We should reflect the runtime behaviour as closely as possible at all times (in my opinion). But we also shouldn't recklessly make changes that might break large chunks of the typing ecosystem, if they don't have a good use case.

I'm personally fine if the consensus here is just "mypy should add some more special-casing". I agree that we should generally stick to the runtime implementation as closely as possible, and this PR would take us further away from that.

@KotlinIsland
Copy link
Contributor Author

Related: python/mypy/issues/12370
Related: python/mypy/issues/12369

@rchen152
Copy link
Collaborator

The primer output indicates that mypy has no problems with this. I'm actually a little surprised at how blasé the CI is about this change.

@erictraut / @rchen152: would this change break pyright or pytype at all?

pytype should be unaffected, since it has its own copies of the builtins and typing stubs.

@AlexWaygood
Copy link
Member

I don't think there's enough consensus to make this change, so I'm closing this. But thank you for the PR; I thought it was an interesting and creative idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(🐞) isinstance works with typing.Unions on 3.10+

5 participants