파이썬(Python) 컴파일, pyc 파일이 생기는 과정 직접 해 보니
맥 한 대에서 파이썬 3.15.0으로 7단계를 한 번씩 실행해 파이썬 컴파일 과정을 확인했습니다. 모듈을 불러올 때 생기는 pyc 파일, 미리 컴파일하기, 실행 전 문법 오류 찾기, 바이트코드 보기, pyc 직접 실행까지 명령과 출력 그대로 담았습니다.
Finn ·

main.py에서 import greet 한 줄을 실행하자 폴더에 __pycache__/greet.cpython-315.pyc가 생겼습니다. 파이썬은 따로 빌드하지 않아도 불러온 모듈을 바이트코드로 바꿔 캐시에 둡니다. 그래서 파이썬 컴파일은 생각보다 가까이에서 일어나고 있죠. 아래 기록은 맥(darwin arm64) 한 대에서 Homebrew로 설치된 python3(3.15.0)로 일곱 단계를 한 번씩 실행한 결과입니다.
따라 할 순서 한눈에 보기
새 빈 폴더 하나를 만들고 다음 순서대로 실행하면 같은 과정을 확인할 수 있습니다.
- 파이썬 버전을 확인합니다.
- 모듈 하나를 불러오는 스크립트를 실행하고
__pycache__를 살펴봅니다. py_compile로 파일 하나를 미리 컴파일합니다.compileall로 폴더 전체를 컴파일합니다.- 괄호를 빠뜨린 파일을 컴파일해 문법 오류를 실행 전에 찾습니다.
dis로 함수의 바이트코드를 출력합니다.- 만들어진
.pyc파일을 직접 실행합니다.

첫 두 단계부터 보면 pyc 파일이 언제 생기는지 바로 알 수 있습니다.
함께 읽을 글
모듈을 불러오면 pyc 파일이 생기나요?
먼저 버전을 확인합니다. 파일 이름에 버전이 들어가기 때문에 이 값이 중요해요.
$ python3 --version
Python 3.15.0이제 greet.py를 만들고, 이를 불러오는 main.py를 실행합니다.
$ printf 'def hello():\n return "안녕"\n' > greet.py && printf 'import greet\nprint(greet.hello())\n' > main.py && python3 main.py && ls __pycache__
안녕
greet.cpython-315.pyc그런데 이름 가운데 cpython-315는 무엇일까요? py_compile 공식 문서에 따르면 바이트코드의 기본 저장 경로는 .pyc로 끝나는 PEP 3147/PEP 488 경로입니다. 문서의 예처럼 baz.py라면 __pycache__/baz.cpython-32.pyc가 되죠. 이번 기록에서는 3.15.0이 315로 붙었습니다. 직접 실행한 main.py가 아니라 불러온 greet.py만 캐시에 남았다는 점도 눈여겨보세요.
실행하지 않고 미리 컴파일하는 방법
그렇다면 불러오기 전에 미리 만들 수도 있을까요? 파일 하나는 py_compile로 컴파일합니다.
$ printf 'print("직접 컴파일")\n' > hello.py && python3 -m py_compile hello.py && ls __pycache__
greet.cpython-315.pyc
hello.cpython-315.pyc폴더 단위라면 compileall이 맞습니다. -q는 컴파일된 파일 목록을 출력하지 않는 옵션입니다.
$ mkdir -p pkg && printf 'X = 1\n' > pkg/a.py && printf 'Y = 2\n' > pkg/b.py && python3 -m compileall -q pkg && ls pkg/__pycache__
a.cpython-315.pyc
b.cpython-315.pyc두 도구는 대상을 고르는 방식이 다릅니다. py_compile 명령줄은 디렉터리를 뒤지지 않고 이름을 적은 파일만 컴파일합니다. 반면 compileall은 넘긴 디렉터리를 재귀적으로 탐색하죠. 공식 문서는 두 모듈 모두 소스 폴더에 쓰기 권한이 없는 사용자가 있을 때 설치 단계에서 캐시를 미리 만드는 용도를 설명합니다.

실행 전에 문법 오류를 찾으려면?
AI 에이전트가 여러 파일을 한꺼번에 고쳤다면, 실행 전에 문법이 깨진 곳을 먼저 찾고 싶을 거예요. 예를 들어 닫는 괄호를 빠뜨린 파일을 컴파일해 보면 이렇습니다.
$ printf 'print("닫는 괄호 없음"\n' > bad.py && python3 -m py_compile bad.py
File "bad.py", line 1
print("닫는 괄호 없음"
^
SyntaxError: '(' was never closed이 단계의 종료 코드는 1이었습니다. 공식 문서도 컴파일할 수 없는 파일이 있으면 종료 상태가 0이 아니라고 밝힙니다. 그래서 스크립트나 커밋 전 점검에 끼워 넣기 좋죠.
이럴 때는 옵션을 조심하세요. py_compile의 -q는 에러 출력 자체를 억제합니다. compileall의 -q는 한 번만 주면 에러 메시지는 그대로 보이고, -qq로 두 번 주면 모든 출력이 사라집니다. 오류를 눈으로 확인하려면 compileall -q 정도가 알맞다고 판단합니다.
바이트코드는 어떻게 생겼나요?
.pyc 안에 무엇이 들었는지 궁금하다면 dis 모듈로 함수의 바이트코드를 볼 수 있습니다.
$ python3 - <<'PY'
import dis
def add(a, b):
return a + b
dis.dis(add)
PY
2 RESUME 0
3 LOAD_FAST_BORROW_LOAD_FAST_BORROW 1 (a, b)
BINARY_OP 0 (+)
RETURN_VALUE왼쪽 숫자는 소스의 줄 번호이고, return a + b 한 줄이 명령 세 개로 나뉘어 나옵니다. 더하기는 BINARY_OP 0 (+)로 표시됐죠. 이 출력은 3.15.0에서 얻은 것이므로, 다른 버전에서는 같은 모양이라고 단정하지 마세요.

pyc 파일을 직접 실행할 수 있나요?
네, 이번 기록에서는 됐습니다. 3단계에서 만든 캐시 파일을 python3에 바로 넘깁니다.
$ python3 __pycache__/hello.cpython-*.pyc
직접 컴파일다만 이 명령도 python3로 실행했다는 점을 기억하세요. .pyc는 파이썬이 읽는 바이트코드일 뿐, 파이썬 없이 도는 실행 파일이 아닙니다.
소스를 고치면 pyc 파일은 어떻게 되나요?
캐시가 오래된 내용을 실행하지는 않을까 걱정될 수 있습니다. 공식 문서는 .pyc가 최신인지 판단하는 세 방식을 설명합니다.
| 방식 | pyc에 담는 것 | 실행할 때 |
|---|---|---|
TIMESTAMP | 소스의 타임스탬프와 크기 | 소스 메타데이터와 비교 |
CHECKED_HASH | 소스 내용의 해시 | 소스와 비교 |
UNCHECKED_HASH | 소스 내용의 해시 | 최신이라고 가정, 검증 안 함 |
SOURCE_DATE_EPOCH 환경 변수가 없으면 기본은 timestamp이고, 설정돼 있으면 checked-hash입니다. 타임스탬프가 최신이어도 다시 만들고 싶다면 compileall -f로 강제할 수 있어요.

측정 조건과 이 기록이 다루지 않는 것
- 환경: 맥 한 대(darwin arm64), Homebrew로 설치된
python33.15.0, 새 임시 폴더 - 횟수: 각 명령을 한 번씩만 실행했습니다. 단계별 시간은 모두 0.0초로 기록돼 속도 비교에는 쓸 수 없습니다.
- 종료 코드: 5단계(문법 오류)만 1이고 나머지는 0이었습니다.
- 다루지 않은 것: 실행 파일(.exe 등)로 묶는 도구는 이번에 실행하지 않았고 두 공식 문서에도 나오지 않아 결과를 말할 수 없습니다.
AI 코딩 도구로 혼자 작업하고 있다면, 에이전트가 고친 파일 이름을 python3 -m py_compile 뒤에 붙여 컴파일하고 종료 코드가 0인지 확인하는 편이 좋습니다. 이번 5단계처럼 문법 오류가 있으면 종료 코드가 1로 나오므로 실행 전에 걸러 낼 수 있고, py_compile 문서도 컴파일할 수 없는 파일이 있으면 종료 상태가 0이 아니라고 밝힙니다. compileall -q는 이번에 정상 파일만 든 폴더에 돌렸기 때문에, 문법 오류가 있는 폴더에서 종료 코드가 어떻게 나오는지는 확인하지 않았습니다. 폴더 단위로 점검하려면 문법 오류 파일을 넣은 폴더에 직접 돌려 종료 코드부터 확인하는 편이 좋습니다.
참고 자료
- py_compile — 파이썬 소스 파일 컴파일 docs.python.org
- compileall — 파이썬 라이브러리 바이트 컴파일하기 docs.python.org