Python .pyc는 언제 다시 만들어질까: timestamp와 hash 기반 무효화
CPython은 기본적으로 소스의 수정 시각과 크기로 .pyc가 최신인지 판단합니다. 이 방식이 놓칠 수 있는 경우와 checked-hash, unchecked-hash .pyc의 차이, compileall 사용법을 Python 3.14.8 문서 기준으로 정리했습니다.
Python 소스를 고쳤는데 실행 결과가 그대로일 때, __pycache__를 지워서 문제가 사라졌다면 오래된 캐시가 원인이었다는 것까지는 알 수 있습니다. 하지만 왜 그 캐시가 쓰였는지는 알 수 없습니다. CPython이 .pyc를 믿어도 된다고 판단하는 방법은 세 가지이고, 어느 방식으로 만들어졌는지에 따라 판단 기준이 다릅니다.
.pyc는 import 캐시다
CPython은 import 중에 소스 모듈을 컴파일해야 하면, 그 바이트코드를 .pyc 파일로 저장해 두었다가 이후 로드에서 재사용할 수 있습니다. Python 3.2에서 PEP 3147이 도입된 뒤로 이 파일은 소스 옆의 __pycache__ 디렉터리에 들어가고, 파일 이름에 인터프리터 태그가 붙습니다. CPython 3.14라면 다음과 같은 모양입니다.
pkg/
├── settings.py
└── __pycache__/
└── settings.cpython-314.pyc
태그는 sys.implementation.cache_tag에서 옵니다. 문서는 관례상 구현 이름과 버전을 합친 값('cpython-33' 같은 형태)이지만 구현마다 다른 값을 쓸 수 있다고 설명합니다. 태그를 나눈 이유는 Python 3.2의 변경 사항 문서에 나옵니다. 예전에는 여러 인터프리터가 같은 mymodule.pyc를 두고 서로 다시 컴파일해 덮어썼습니다. 그래서 .pyc를 다른 Python 버전이나 다른 구현으로 옮겨 쓸 수 있는 파일로 보면 안 됩니다.
특정 소스의 캐시 경로는 표준 라이브러리로 확인할 수 있습니다.
import importlib.util
print(importlib.util.cache_from_source("pkg/settings.py"))
-B 옵션이나 PYTHONDONTWRITEBYTECODE 환경 변수를 쓰면 import할 때 .pyc 파일을 새로 기록하지 않습니다. PYTHONPYCACHEPREFIX(또는 -X pycache_prefix=PATH, 3.8부터)를 지정하면 __pycache__ 대신 별도 경로 아래의 미러 트리에 씁니다.
무효화 방식 세 가지
Python 3.7에서 PEP 552가 .pyc 형식을 확장해 소스 해시로 무효화하는 방식을 추가했습니다. 그 전에는 timestamp 방식만 있었습니다. 어떤 방식으로 만든 .pyc인지는 파일 헤더에 기록되고, 아래 표는 Python 3.14.8 문서에 적힌 현재 동작입니다.
| 방식 | .pyc에 기록되는 것 |
import할 때 하는 일 |
|---|---|---|
| timestamp (기본값) | 소스의 마지막 수정 시각과 크기 | 현재 소스의 수정 시각, 크기와 비교 |
| checked-hash | 소스 내용의 해시 | 소스를 해시해서 비교하고, 다르면 다시 만들어 씀 |
| unchecked-hash | 소스 내용의 해시 | 파일이 있으면 유효하다고 보고 소스와 비교하지 않음 |
해시 값은 importlib.util.source_hash()가 돌려주는 값과 같은 것이 헤더에 들어갑니다. 3.7 변경 사항 문서에 따르면 CPython은 기본적으로 계속 timestamp 방식을 쓰고, import 도중에 hash 기반 .pyc를 만들지 않습니다. hash 기반 .pyc는 py_compile이나 compileall로 만들어야 합니다.
timestamp 방식이 놓치는 경우
timestamp 방식은 소스 내용을 읽어 비교하지 않습니다. 기록해 둔 수정 시각과 크기가 지금 소스의 값과 같으면 내용이 달라졌어도 캐시를 그대로 씁니다.
예를 들어 settings.py의 VALUE = 1을 VALUE = 2로 바꾸면 파일 크기는 같습니다. 이때 수정 시각까지 예전 값과 같아지는 상황은 이런 경우입니다.
- 파일 시스템의 시각 해상도가 거칠어서, 같은 시각 단위 안에서 수정이 두 번 일어난 경우. 3.7 변경 사항 문서도 timestamp가 너무 거칠면 소스 변경을 놓칠 수 있다고 적고 있습니다.
- 배포나 압축 해제 도구가 원본 파일의 수정 시각을 보존해 복사했고, 대상 쪽에 같은 시각과 크기로 만들어진 다른 내용의
.pyc가 이미 남아 있는 경우. - 빌드 과정에서 모든 파일의 수정 시각을 고정된 값으로 맞추는 경우.
이 조건이 겹치면 import는 아무 오류 없이 이전 바이트코드를 실행합니다. 이 글을 위해 위 상황을 직접 실행해 보지는 않았고, 문서에 적힌 비교 규칙에서 따라 나오는 결과로 설명했습니다.
반대로 내용은 그대로인데 수정 시각만 바뀌면 다시 컴파일합니다. 결과가 틀리지는 않지만 컨테이너 빌드처럼 매번 수정 시각이 바뀌는 환경에서는 캐시가 덜 재사용됩니다.
checked-hash와 unchecked-hash
checked-hash .pyc에서 모듈을 로드할 때 Python은 소스 파일을 읽어 해시를 계산하고 헤더의 값과 비교합니다. 이미 sys.modules에 있는 모듈은 이 검사 없이 그대로 반환되므로, 검사는 import 문을 실행할 때마다가 아니라 모듈을 로드할 때 일어납니다. 해시가 다르면 다시 컴파일하고 새 checked-hash .pyc를 씁니다. 수정 시각에 의존하지 않으므로 앞 절의 상황에서도 내용이 바뀐 것을 알아챕니다. 대신 소스 전체를 읽고 해시하는 비용이 있습니다.
unchecked-hash .pyc는 파일이 있으면 유효하다고 가정합니다. 소스를 고쳐도 .pyc를 다시 만들기 전까지는 예전 바이트코드가 실행됩니다. py_compile 문서는 이 방식이 빌드 시스템처럼 Python 바깥의 시스템이 .pyc를 최신으로 유지할 때 쓸모 있다고 설명합니다. 소스를 직접 고치는 개발 환경에는 맞지 않습니다.
hash 기반 .pyc의 검증 동작은 인터프리터 옵션으로 바꿀 수 있습니다.
python --check-hash-based-pycs always app.py
| 값 | 동작 |
|---|---|
default |
checked는 검증하고 unchecked는 검증하지 않음 |
always |
checked와 unchecked 모두 소스와 비교 |
never |
hash 기반 .pyc를 소스와 비교하지 않음 |
이 옵션은 timestamp 방식 .pyc에는 영향을 주지 않습니다. 배포된 환경에서 unchecked-hash .pyc가 의심될 때 always로 한 번 실행해 결과가 달라지는지 보면 캐시 문제인지 가려내는 데 도움이 됩니다.
무효화 방식을 정해서 컴파일하기
compileall은 디렉터리를 재귀적으로 돌며 컴파일합니다.
python -m compileall --invalidation-mode checked-hash src/
python -m compileall -f -q --invalidation-mode unchecked-hash build/lib/
--invalidation-mode는timestamp,checked-hash,unchecked-hash를 받습니다.-f는 timestamp가 최신이어도 다시 컴파일합니다.-q는 컴파일한 파일 목록을 출력하지 않습니다. 오류 메시지는 남고,-qq면 모두 숨깁니다.
파일 하나는 py_compile로 만들 수 있습니다.
import py_compile
py_compile.compile(
"src/settings.py",
invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
)
기본값은 환경 변수 SOURCE_DATE_EPOCH에 따라 달라집니다. py_compile.compile()과 compileall 모두, 이 변수가 설정되어 있으면 기본값이 checked-hash이고 없으면 timestamp입니다. py_compile은 3.7에서 이 변수가 있으면 지정한 모드를 checked-hash로 덮어썼지만, 3.7.2부터는 기본값만 정합니다. 의도와 다른 방식으로 .pyc가 만들어졌다면 빌드 환경에 이 변수가 설정되어 있는지 먼저 확인해 볼 만합니다.
pycache 삭제는 진단 단계다
__pycache__를 지우고 문제가 사라졌다면, 오래된 캐시가 원인이었다는 사실까지만 확인한 것입니다. 같은 일이 다시 일어나지 않게 하려면 원인을 찾아야 합니다.
- 배포 도구가 수정 시각을 보존하거나 고정하는가
- 빌드 산출물에 들어간
.pyc가 어떤 무효화 방식으로 만들어졌는가 - unchecked-hash
.pyc를 쓰는데 소스만 따로 교체되는 경로가 있는가 - 빌드 환경에
SOURCE_DATE_EPOCH가 설정되어 있는가
어떤 방식을 고를까
- 로컬 개발과 일반적인 실행: 기본값인 timestamp로 충분합니다.
- 수정 시각을 믿기 어렵거나 재현 가능한 빌드가 필요할 때: checked-hash. 3.7 변경 사항 문서도 timestamp가 빌드 재현성과 내용 기반 빌드 시스템에 문제가 된다고 설명합니다.
- 소스와
.pyc가 항상 같은 빌드에서 함께 교체되고, 배포 후 소스를 직접 고치지 않을 때: unchecked-hash. 소스를 따로 패치할 일이 있다면 쓰지 않는 편이 낫습니다.
hash 기반 .pyc는 캐시가 최신인지 판단하는 방법일 뿐입니다. 코드 서명이 아니며, 파일이 변조되지 않았다는 것을 보장하지 않습니다.
이 글의 동작 설명은 2026년 10월 9일 Python 3.14.8 문서를 기준으로 확인했습니다.
참고한 문서
- Python 3.14.8 언어 레퍼런스: Cached bytecode invalidation
- py_compile, compileall, importlib, sys.implementation
- 명령줄과 환경 변수:
--check-hash-based-pycs,-B,PYTHONPYCACHEPREFIX - 변경 사항: Python 3.7 (PEP 552), Python 3.2 (PEP 3147)