`stubgen --inspect-mode` crashes with `AttributeError on PEP 604 union syntax (X | Y)` *(mypy 2.1.0)*
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
Environment
- mypy version: 2.1.0 (compiled: yes)
- Python: 3.11
- OS: Debian/Linux
- Install method: pip, user install (
~/.local/lib/python3.11/site-packages)
Steps to Reproduce
- Create a module using PEP 604 union syntax:
# test_union_pep604.py
def greet(name: str) -> str:
return f"Hello, {name}"
def process(value: int | str) -> float | None:
if isinstance(value, int):
return float(value)
return None
class Container:
def __init__(self, data: list[str] | dict[str, int]) -> None:
self.data = data
def get(self, key: str) -> int | None:
if isinstance(self.data, dict):
return self.data.get(key)
return None
- Run:
stubgen --inspect-mode -o /tmp/stubs_test test_union_pep604.py
Expected Behavior
stubgen generates a .pyi stub file preserving the X | Y annotations, the same way it succeeds on the equivalent Union[X, Y] syntax:
stubgen --inspect-mode -o /tmp/stubs_test test_union_old.py
# → Generated /tmp/stubs_test/test_union_old.pyi (exit 0)
Actual Behavior
stubgen crashes with an unhandled AttributeError and exits with code 1:
Traceback (most recent call last):
File "/home/<user>/.local/bin/stubgen", line 6, in <module>
sys.exit(main())
File "mypy/stubgen.py", line 2057, in main
File "mypy/stubgen.py", line 1860, in generate_stubs
File "mypy/stubgen.py", line 1818, in generate_stub_for_py_module
File "/home/<user>/.local/lib/python3.11/site-packages/mypy/stubgenc.py", line 452, in generate_module
self.generate_function_stub(name, obj, output=functions)
File "/home/<user>/.local/lib/python3.11/site-packages/mypy/stubgenc.py", line 631, in generate_function_stub
default_sig = self.get_default_function_sig(obj, ctx)
File "/home/<user>/.local/lib/python3.11/site-packages/mypy/stubgenc.py", line 344, in get_default_function_sig
add_args(args, get_pos_default)
File "/home/<user>/.local/lib/python3.11/site-packages/mypy/stubgenc.py", line 336, in add_args
arglist.append(ArgSig(arg, get_annotation(arg), default=False))
File "/home/<user>/.local/lib/python3.11/site-packages/mypy/stubgenc.py", line 311, in get_annotation
return self.get_type_fullname(argtype)
File "/home/<user>/.local/lib/python3.11/site-packages/mypy/stubgenc.py", line 775, in get_type_fullname
typename = getattr(typ, "__qualname__", typ.__name__)
AttributeError: 'types.UnionType' object has no attribute '__name__'
Root Cause
--inspect-mode inspects live runtime objects rather than parsing source. When Python evaluates a X | Y annotation, the resulting object is an instance of types.UnionType (introduced in Python 3.10, PEP 604). stubgenc.py's get_type_fullname assumes any annotation object falls back to having a __name__, which types.UnionType does not define — hence the crash.
Proposed Fix
File: mypy/stubgenc.py
1. Import UnionType (near line 25):
- from types import FunctionType, ModuleType
+ from types import FunctionType, ModuleType, UnionType
2. Special-case it in get_type_fullname (near line 775):
if typ is Any:
return "Any"
+ if isinstance(typ, UnionType):
+ return " | ".join(self.get_type_fullname(a) for a in typ.__args__)
typename = getattr(typ, "__qualname__", typ.__name__)
This recursively resolves each member of the union and rejoins them with |, matching the original annotation's syntax. types.UnionType is available in all Python versions mypy 2.1.0 targets (3.10+), so no version gating is needed.
Verification
With the patch applied, test_union_pep604.py generates a stub with unions preserved:
$ stubgen --inspect-mode -o /tmp/stubs_test test_union_pep604.py
Processed 1 modules
Generated /tmp/stubs_test/test_union_pep604.pyi
$ cat /tmp/stubs_test/test_union_pep604.pyi
def greet(name: str) -> str: ...
def process(value: int | str) -> float | None: ...
class Container:
def __init__(self, data: list | dict) -> None: ...
def get(self, key: str) -> int | None: ...
test_union_old.py (using typing.Union) no longer crashes either, but it does not preserve the union — it falls back to Incomplete for those annotations, which is a pre-existing, separate limitation of runtime inspection unrelated to this patch:
Existing Issue
test_union_old.py
"""Module using classic Union[X, Y] syntax — should work with --inspect-mode."""
from typing import Union, Optional
def greet(name: str) -> str:
return f"Hello, {name}"
def process(value: Union[int, str]) -> Optional[float]:
if isinstance(value, int):
return float(value)
return None
class Container:
def __init__(self, data: Union[list[str], dict[str, int]]) -> None:
self.data = data
def get(self, key: str) -> Optional[int]:
if isinstance(self.data, dict):
return self.data.get(key)
return None
produces
$ stubgen --inspect-mode -o /tmp/stubs_test test_union_old.py
Processed 1 modules
Generated /tmp/stubs_test/test_union_old.pyi
$ cat /tmp/stubs_test/test_union_old.pyi
from _typeshed import Incomplete
def greet(name: str) -> str: ...
def process(value) -> Incomplete: ...
class Container:
def __init__(self, data) -> None: ...
def get(self, key: str) -> Incomplete: ...
So: the patch fixes the crash for PEP 604 syntax and, as a bonus, correctly resolves those unions in the stub. It does not fix typing.Union resolution — that gap already existed before this patch and is out of scope here.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in mypy/stubgenc.py, especially get_type_fullname and the inspect-mode path shown in the traceback. Run stubgen --inspect-mode with the provided test_union_pep604.py example and compare the generated stub with the expected output. Done means the command exits successfully and preserves PEP 604 union annotations without regressing the existing behavior described in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100