ty icon indicating copy to clipboard operation
ty copied to clipboard

support try/except imports of typing special forms

Open carljm opened this issue 2 months ago • 4 comments

Some projects (at least graphql-core) import typing special forms like this, rather than using sys.version_info checks:

try:
    from typing import TypeGuard
except ImportError:
    from typing_extensions import TypeGuard

We handle this fine on Python versions in which typing.TypeGuard exists (in that case typing.TypeGuard and typing_extensions.TypeGuard are the same object, so the union resolves to just that object.) We don't handle it when checking on an older Python where the typing_extensions fallback is necessary. In that case we emit a diagnostic on the import from typing, of course, and then we end up with a union of Unknown | typing_extensions.TypeGuard, which our special-form-handling code does not treat as TypeGuard.

Mypy, pyright, and pyrefly don't handle this either.

The one thing we do differently (since https://github.com/astral-sh/ruff/pull/21503) is that we also emit a diagnostic on e.g. TypeGuard[int] in an annotation, when the TypeGuard symbol is Unknown | typing_extensions.TypeGuard. Arguably this is good, since it alerts you to the fact that your typeguard function is not actually doing any narrowing, and gives you a better clue as to why? But we could also silence this.

If we wanted to support this pattern, some options could be:

  1. Understand try... except ImportError as a statically-known branch, given existence / non-existence of the import. This would also help us in other similar import-fallback cases.
  2. Handle the union with Unknown as a special case in our type expression parsing. (This might result in false negatives if the union with Unknown comes from a weirder source that we wouldn't want to support?)

carljm avatar Nov 18 '25 18:11 carljm

I encountered a case of this with library mypy_boto3_s3 (pypi, github generator code). It uses a try / except ImportError pattern, I think for Python backwards compatibility?

# mypy_boto3_s3/__init__.py
try:
    from .service_resource import S3ServiceResource
except ImportError:
    from builtins import object as S3ServiceResource  # type: ignore[assignment]

Unfortunately, ty doesn't handle this as gracefully as mypy. Mypy seems to recognize my var: mypy_boto3_s3.S3ServiceResource as an instance of mypy_boto3_s3.service_resource.S3ServiceResource, but ty instead widens it to object, and then it doesn't type check my code correctly.

I have no suggestion on how to handle this correctly, I am simply reporting that I encountered this issue in the wild.

Here's a basic reproduction case:

# mypy_boto3_s3_test.py
from mypy_boto3_s3 import S3ServiceResource

def f() -> S3ServiceResource:
    """Fake making an S3ServiceResource"""
    return None  # type: ignore

s = f()
s.get_available_subresources()  # ty thinks this is an error

$ uvx ty version
ty 0.0.2

$ uvx ty check mypy_boto3_s3_test.py 
error[unresolved-attribute]: Object of type `object` has no attribute `get_available_subresources`
 --> mypy_boto3_s3_test.py:9:1
  |
8 | s = f()
9 | s.get_available_subresources()  # ty thinks this is an error
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
info: rule `unresolved-attribute` is enabled by default

Found 1 diagnostic

leon-sony avatar Dec 18 '25 00:12 leon-sony

# mypy_boto3_s3/__init__.py
try:
    from .service_resource import S3ServiceResource
except ImportError:
    from builtins import object as S3ServiceResource  # type: ignore[assignment]

I can't find this code anywhere in the linked repository?

sharkdp avatar Dec 18 '25 09:12 sharkdp

I can't find this code anywhere in the linked repository?

I think the linked package is a code generator, which generates code like this.

carljm avatar Dec 19 '25 01:12 carljm

I created #2096 to discuss/track the issue reported by @leon-sony, since it's not actually the same as what this issue is about. (This issue is specifically about handling special form types from the typing module which are imported using a try/except fallback; mypy also doesn't handle this.)

carljm avatar Dec 19 '25 01:12 carljm