파이썬 파일은 있는데 FileNotFoundError가 날 때: 실행 위치와 상대경로 확인 순서

파이썬 파일은 실제로 존재하는데 FileNotFoundError가 난다면, 먼저 파일이 있는 폴더가 아니라 현재 작업 폴더를 확인해야 합니다. 상대경로는 실행한 .py 파일의 위치가 아니라 현재 작업 폴더를 기준으로 해석되기 때문입니다. 그다음 cwd, 해석된 대상 경로, 파일과 부모 디렉터리의 존재 여부를 차례로 확인하면 실행 방식 문제와 실제 경로 문제를 나눌 수 있습니다.

상대경로의 기준점부터 확인하기

다음 코드로 파이썬 프로세스가 보고 있는 현재 작업 폴더와 상대경로의 절대경로를 출력할 수 있습니다.

import os
from pathlib import Path

relative_input = Path("data/input.csv")

print("cwd      :", os.getcwd())
print("Path.cwd :", Path.cwd())
print("target   :", relative_input.resolve())
print("exists   :", relative_input.exists())
print("is_file  :", relative_input.is_file())

Path.cwd()는 현재 작업 폴더를 반환합니다. Path.resolve()는 경로를 절대경로로 만들면서 ..와 심볼릭 링크를 해석하므로, 실제로 어느 파일을 찾고 있는지 확인할 때 유용합니다. 다만 resolve()가 보여 주는 대상이 기대한 폴더가 아니라면, 파일명부터 고치기보다 실행 방식이 현재 작업 폴더를 어떻게 정했는지 먼저 살펴보는 편이 빠릅니다.

같은 확인을 os.path로 작성할 수도 있습니다.

import os

relative_input = "data/input.csv"
print("cwd    :", os.getcwd())
print("target :", os.path.abspath(relative_input))

os.path.abspath()도 현재 작업 폴더를 기준으로 상대경로를 절대경로로 바꾸지만, 심볼릭 링크 자체를 해석하지는 않습니다. 운영 환경에서 링크가 포함될 수 있다면 두 결과의 차이도 함께 확인해야 합니다.

파일이 있는지와 부모 폴더가 있는지 분리하기

오류 메시지에 나온 경로를 그대로 확인하되, 입력 파일과 그 부모 폴더를 나누어 점검합니다.

from pathlib import Path

input_path = Path("data/input.csv").resolve()
parent_path = input_path.parent

print("input:", input_path)
print("parent exists:", parent_path.exists())
print("parent is_dir:", parent_path.is_dir())
print("file exists:", input_path.exists())
print("is_file:", input_path.is_file())

부모 폴더가 없다면 현재 작업 폴더가 예상과 다르거나 폴더 이름이 틀렸을 가능성이 큽니다. 부모 폴더는 있지만 파일만 없다면 파일명, 확장자, 대소문자, 생성 시점과 입력 위치를 확인합니다. exists()만으로 끝내지 말고 is_file()과 is_dir()를 함께 써야 디렉터리를 파일로 착각하는 경우도 걸러낼 수 있습니다.

__file__ 기준 경로가 맞는 경우

입력 파일이 항상 스크립트와 함께 배포되는 구조라면 스크립트 파일 위치를 기준으로 경로를 만들 수 있습니다.

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
input_path = BASE_DIR / "data" / "input.csv"

if not input_path.is_file():
    raise FileNotFoundError(f"입력 파일을 찾을 수 없습니다: {input_path}")

print(input_path)

이 방식은 터미널의 현재 폴더나 IDE의 실행 설정이 달라도 스크립트 옆의 data 폴더를 찾고 싶을 때 적합합니다. 다만 __file__은 항상 존재하는 값이 아닙니다. 파일에서 모듈을 읽은 실행 맥락에서 제공되는 선택적 속성이므로, 대화형 실행이나 일부 로드 방식에서는 이 코드를 그대로 사용할 수 없습니다.

반대로 사용자가 입력 파일을 지정하는 도구라면 스크립트 기준으로 고정하기보다 명시적 입력 인자를 받는 편이 더 분명합니다.

import argparse
from pathlib import Path

parser = argparse.ArgumentParser()
parser.add_argument("input", type=Path)
args = parser.parse_args()

input_path = args.input.expanduser().resolve()
if not input_path.is_file():
    raise FileNotFoundError(f"입력 파일을 찾을 수 없습니다: {input_path}")

print(input_path)

정리하면, 프로젝트에 포함된 고정 입력은 __file__ 기준 경로가 이해하기 쉽고, 실행할 때마다 달라지는 입력은 명령줄 인자가 적합합니다. 단순한 상대경로를 유지할 때는 실행 문서나 작업 스케줄러 설정에서 현재 작업 폴더를 명시해야 합니다.

실행 위치 문제인지 판단하는 순서

저라면 다음 순서로 좁혀 갑니다.

  1. Path.cwd()로 현재 작업 폴더를 출력합니다.
  2. Path("상대경로").resolve()로 실제 조회 대상의 절대경로를 출력합니다.
  3. 대상 부모 폴더에 exists()와 is_dir()를 적용합니다.
  4. 대상에 exists()와 is_file()을 적용합니다.
  5. 대상 경로가 틀렸다면 상대경로, 스크립트 기준 경로, 명시적 입력 인자 중 하나로 설계를 바꿉니다.

이 순서를 지키면 “파일이 있는데 왜 못 찾지?”라는 막연한 추측을 실행 기준점과 파일 구조의 문제로 나누어 볼 수 있습니다. 특히 IDE, 다른 터미널, 작업 스케줄러처럼 실행 주체가 바뀌는 자동화에서는 코드보다 먼저 cwd와 최종 경로를 로그에 남기는 습관이 재현에 도움이 됩니다.

출처

Leave a Comment