Skip to content

Subscripted PEP 695 generic type alias skips the check entirely, so any value passes #579

Description

@ebarkhordar

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 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions