1. 문제 정의

검증 환경

본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.

  • 본문에 명시된 오류 메시지·프레임워크 버전을 기준으로 원인을 추적했다.
  • 문서 정리일: 2026-09-01
  • OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.

asyncio.print_call_graph()는 실행 중인 태스크의 호출 그래프를 텍스트로 출력하는 디버깅용 헬퍼 함수입니다. 그런데 이미 완료된 태스크에 대해 호출하면 그래프를 출력하는 대신 AttributeError로 크래시하는 버그가 있습니다.

에러 원문: asyncio.print_call_graph() raises AttributeError on a finished task

1
AttributeError: 'NoneType' object has no attribute 'f_generator'

출처: python/cpython #156408

이슈에 제시된 재현 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
import asyncio

async def main():
    t = asyncio.create_task(asyncio.sleep(0))
    await t
    asyncio.print_call_graph(t)

asyncio.run(main())

핵심은 태스크 t를 await로 완료시킨 뒤 print_call_graph(t)를 호출하는 것입니다.

  • 실제 출력:
    1
    
    AttributeError: 'NoneType' object has no attribute 'f_generator'
    
  • 기대 출력:
    1
    
    * Task(name='Task-2', id=0x103fc49b0)
    

2. 원인 탐구 (표면 증상)

에러 메시지의 f_generator는 프레임(frame)에서 코루틴으로 거슬러 올라가는 속성입니다. AttributeError: 'NoneType' object has no attribute 'f_generator'라는 점에서, print_call_graph() 내부가 코루틴의 프레임 객체에 접근하다가 그 대상이 None임을 만났음을 짐작할 수 있습니다.

즉 표면적으로는 “그래프 그리기가 실패했다"이지만, 이면에는 코루틴 프레임이 None이 될 수 있다는 사실을 처리하지 못하는 검사 누락이 있습니다.

증상 fingerprint

항목내용
에러 메시지AttributeError: 'NoneType' object has no attribute 'f_generator'
발생 단계태스크가 완료된 이후 print_call_graph() 호출 시점
관련 도구Python 표준 라이브러리 asyncio, CPython 코루틴 프레임 구조
흔한 오해“태스크 객체가 잘못됐다”, “인자를 잘못 넘겼다” — 둘 다 아님

3. 근본 원인 분석

print_call_graph()는 호출 그래프를 그릴 때 코루틴 체인을 따라 프레임을 수집합니다. 이슈에 제안된 수정 코드의 구조에서 내부 로직을 유추하면 다음과 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
while coro is not None:
    if hasattr(coro, 'cr_await'):
        # A native coroutine or duck-type compatible iterator
        st.append(FrameCallGraphEntry(coro.cr_frame))   # 크래시 지점
        coro = coro.cr_await
    elif hasattr(coro, 'ag_await'):
        # A native async generator or duck-type compatible iterator
        st.append(FrameCallGraphEntry(coro.ag_frame))
        coro = coro.ag_await
    else:
        break

여기서 coro.cr_frameNone일 수 있는데, 이 사실을 검사하지 않고 곧바로 FrameCallGraphEntry(coro.cr_frame)에 넘기기 때문에 내부에서 프레임의 f_generator에 접근하려다 NoneType AttributeError가 발생합니다.

왜 완료된 코루틴의 cr_frame은 None이 되는가

CPython에서 네이티브 코루틴의 cr_frame은 코루틴이 아직 완료되지 않았을 때만 활성 프레임을 가리킵니다. 코루틴이 끝나면 실행 프레임이 해제되면서 cr_frameNone이 됩니다.

재현 코드에서 await t로 태스크 t가 실행을 마쳤기 때문에, 그 시점의 t가 실행하던 코루틴의 cr_frame은 이미 None입니다. print_call_graph()가 이 None을 감지하지 못하고 프레임을 만들려다 크래시한 것입니다.

원인 후보 매트릭스

원인 후보확인 방법맞는 경우의 증상해결 방향
코루틴 체인(cr_await)이 None루프에서 coro 값 추적while 문에서 조용히 종료됨, 크래시 아님해당 없음
프레임(cr_frame)이 None (본 사례)완료된 태스크의 cr_frame 확인None 출력 + f_generator AttributeErrorcr_frame None 검사 후 프레임 추가 건너뛰기
태스크 객체가 잘못됨isinstance(t, asyncio.Task)타입 오류올바른 태스크 전달

본 사례는 가운데 행에 해당합니다. cr_frameNone인지 검사하는 로직이 빠져 있는 것이 근본 원인입니다.


4. 코드 해결책

이슈 본문에 제안된 수정은 네이티브 코루틴 분기에서 cr_frameNone이 아닐 때만 프레임을 추가하고, 그 다음 코루틴(cr_await)으로 이동하도록 하는 것입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
while coro is not None:
    if hasattr(coro, 'cr_await'):
        # A native coroutine or duck-type compatible iterator
        if coro.cr_frame is not None:
            st.append(FrameCallGraphEntry(coro.cr_frame))
        coro = coro.cr_await
    elif hasattr(coro, 'ag_await'):
        st.append(FrameCallGraphEntry(coro.ag_frame))
        coro = coro.ag_await
    else:
        break

변경은 두 부분입니다.

  1. 기존: st.append(FrameCallGraphEntry(coro.cr_frame)) — None이든 아니든 무조건 추가 시도
  2. 수정: if coro.cr_frame is not None: st.append(...) — None이 아닐 때만 추가

이렇게 하면 이미 완료되어 cr_frame이 None이 된 코루틴에서도 프레임을 건너뛰고 cr_await를 따라 계속 탐색할 수 있어 크래시 없이 그래프를 그릴 수 있습니다.

검증 명령

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import asyncio

async def main():
    t = asyncio.create_task(asyncio.sleep(0))
    await t
    # 수정 전: AttributeError
    # 수정 후: * Task(name='Task-2', id=0x103fc49b0)
    asyncio.print_call_graph(t)

asyncio.run(main())

실행 결과로 NoneType AttributeError가 더는 발생하지 않고 태스크 그래프가 출력되는지 확인하면 됩니다.


5. 향후 예방 조치

실전 적용 체크

환경확인할 점
로컬 개발완료된 태스크를 디버깅 헬퍼에 넘길 때 cr_frame/ag_frame의 None 처리를 확인
CIasyncio 디버깅 API를 쓰는 테스트에 완료된 태스크 경로를 포함해 회귀 테스트
프로덕션코루틴 프레임 접근은 진단 코드에 한정하고 코루틴을 불필요하게 오래 보관하지 않기

잘못된 해결책 (권장하지 않음)

  • 완료된 코루틴을 참조로 오래 살려두기: cr_frame을 유지하려고 코루틴/태스크를 장기 보관하는 것은 메모리와 동작을 왜곡할 수 있습니다. 이 문제는 호출 그래프 도구의 None 검사 누락이므로 도구 쪽에서 None을 처리해야 합니다.
  • 예외를 try/except로 무시하며 넘기기: 그래프가 그려지지 않는 채 예외만 삼키면 진짜 버그를 숨기고 도구가 목적한 호출 그래프 출력을 달성하지 못합니다.

버전별 주의

이 이슈는 CPython main 브랜치에서 asyncio.print_call_graph()를 사용할 때 확인되었습니다. print_call_graph()는 모든 지원 버전에 항상 존재하지 않을 수 있으므로, 사용 전에 해당 함수가 지원되는지 확인하는 것이 안전합니다. (이는 문제 원인이 아니라 일반적인 점검 기준입니다.)


DevTrace verdict

핵심은 완료된 코루틴의 cr_frame이 None이 될 수 있는데 asyncio.print_call_graph()가 이를 검사하지 않고 프레임을 만들려다 f_generator 접근에서 AttributeError를 던진다는 None 체크 누락입니다.

DevTrace 결론

핵심은 코루틴 자체가 사라진 것이 아니라, 완료된 코루틴의 cr_frame이 None이 되는데 호출 그래프 탐색 루프가 그 사실을 처리하지 못하는 검사 누락이다.

원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.

출처