Skip to content

annotationlib.get_annotations() infinite loop on __wrapped__ cycle (eval_str=True) #146556

@raminfp

Description

@raminfp

Bug report

Bug description:

annotationlib.get_annotations() hangs indefinitely when called with eval_str=True on a callable that has a circular __wrapped__ reference chain.

Reproducer

import annotationlib

def f(x: 'int') -> 'str': pass
f.__wrapped__ = f  # self-referential cycle

# Hangs forever, never returns
annotationlib.get_annotations(f, eval_str=True)

Two-node cycle also triggers it:

def g(): pass
f.__wrapped__ = g
g.__wrapped__ = f
annotationlib.get_annotations(f, eval_str=True)  # hangs

Root Cause

In Lib/annotationlib.py, get_annotations() has an early-return guard at line 1010:

if not eval_str:
    return dict(ann)   # fast path, skips unwrap entirely for default eval_str=False

When eval_str=True the code falls through to a while True: loop (lines 1039–1048)
that unwraps __wrapped__ chains with no cycle detection:

if unwrap is not None:
    while True:
        if hasattr(unwrap, "__wrapped__"):
            unwrap = unwrap.__wrapped__   #  no cycle detection
            continue
        if functools := sys.modules.get("functools"):
            if isinstance(unwrap, functools.partial):
                unwrap = unwrap.func      #  also no cycle detection
                continue
        break

Fix

Apply the same cycle-detection pattern used by inspect.unwrap() (visited id-set):

if unwrap is not None:
    seen = {id(unwrap)}
    while True:
        if hasattr(unwrap, "__wrapped__"):
            candidate = unwrap.__wrapped__
            if id(candidate) in seen:
                break
            seen.add(id(candidate))
            unwrap = candidate
            continue
        if functools := sys.modules.get("functools"):
            if isinstance(unwrap, functools.partial):
                candidate = unwrap.func
                if id(candidate) in seen:
                    break
                seen.add(id(candidate))
                unwrap = candidate
                continue
        break
    if hasattr(unwrap, "__globals__"):
        obj_globals = unwrap.__globals__

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions