`stubgen --inspect-mode` crashes with `AttributeError on PEP 604 union syntax (X | Y)` *(mypy 2.1.0)*
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
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.
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.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 56
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.
More from python/mypy
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
bug topic-configuration topic-error-reporting
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
bug topic-attrs
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100