Things to check first
Typeguard version
4.6.0 (also present on main at 9f289c7)
Python version
3.12.14, 3.13.15 and 3.14.7 (all three behave the same)
What happened?
When an annotation is a PEP 695 generic type alias with its type parameters filled in, such as Boxed[int] for type Boxed[T] = list[T], no check is performed at all. Every value passes, including values of a completely unrelated type, and no error or warning is emitted.
A type alias without type parameters is checked correctly, so the two sit side by side in the same module and only one of them catches anything:
check_type(["a"], Plain) # type Plain = list[int] -> TypeCheckError, correct
check_type("hello", Boxed[int]) # type Boxed[T] = list[T] -> returns "hello"
I expected check_type(["a"], Boxed[int]) to raise TypeCheckError the same way check_type(["a"], list[int]) does, and I expected @typechecked to reject the call in the example below.
The same silence shows up wherever the parameterized alias appears, including nested inside another annotation:
check_type([["a"]], list[Boxed[int]]) # returns [['a']]
check_type({"k": ["a"]}, dict[str, Boxed[int]]) # returns {'k': ['a']}
check_type(["a"], Boxed[int] | None) # returns ['a']
This is a silent false negative rather than a crash, which is what makes it awkward in practice: a codebase that annotates with its own parameterized aliases gets no runtime checking on those annotations and nothing says so.
Possibly related: #541, where a type alias without parameters was not being resolved. That one is fixed and the non-parameterized case above confirms it.
How can we reproduce the bug?
No third party libraries. pip install typeguard in a clean Python 3.12, 3.13 or 3.14 environment, then run:
from typeguard import TypeCheckError, check_type, typechecked
type Plain = list[int]
type Boxed[T] = list[T]
def show(label, fn):
try:
fn()
print("no error |", label)
except TypeCheckError as exc:
print("TypeCheckError|", label, "|", exc)
# control: an alias with no type parameters is checked
show("Plain <- ['a'] ", lambda: check_type(["a"], Plain))
# the bug: the same list is accepted against a parameterized alias
show("Boxed[int] <- ['a'] ", lambda: check_type(["a"], Boxed[int]))
show("Boxed[int] <- 'hello'", lambda: check_type("hello", Boxed[int]))
@typechecked
def f(x: Boxed[int]) -> Boxed[int]:
return x
show("f('hello') ", lambda: f("hello"))
Output on typeguard 4.6.0 / Python 3.13.15:
TypeCheckError| Plain <- ['a'] | item 0 of list is not an instance of int
no error | Boxed[int] <- ['a']
no error | Boxed[int] <- 'hello'
no error | f('hello')
Only the first line raises. The last three all pass.
Things to check first
I have searched the existing issues and didn't find my bug already reported there
I have checked that my bug is still present in the latest release
Typeguard version
4.6.0 (also present on
mainat 9f289c7)Python version
3.12.14, 3.13.15 and 3.14.7 (all three behave the same)
What happened?
When an annotation is a PEP 695 generic type alias with its type parameters filled in, such as
Boxed[int]fortype Boxed[T] = list[T], no check is performed at all. Every value passes, including values of a completely unrelated type, and no error or warning is emitted.A type alias without type parameters is checked correctly, so the two sit side by side in the same module and only one of them catches anything:
I expected
check_type(["a"], Boxed[int])to raiseTypeCheckErrorthe same waycheck_type(["a"], list[int])does, and I expected@typecheckedto reject the call in the example below.The same silence shows up wherever the parameterized alias appears, including nested inside another annotation:
This is a silent false negative rather than a crash, which is what makes it awkward in practice: a codebase that annotates with its own parameterized aliases gets no runtime checking on those annotations and nothing says so.
Possibly related: #541, where a type alias without parameters was not being resolved. That one is fixed and the non-parameterized case above confirms it.
How can we reproduce the bug?
No third party libraries.
pip install typeguardin a clean Python 3.12, 3.13 or 3.14 environment, then run:Output on typeguard 4.6.0 / Python 3.13.15:
Only the first line raises. The last three all pass.