Skip to content

Python Pathlib: 현대적인 파일 경로 처리 가이드

Updated on

만약 os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'output')와 같은 Python 코드를 작성해 본 적이 있다면, 이미 문제를 알고 있는 것입니다. os.path를 사용한 문자열 기반 파일 경로 조작은 장황하고, 읽기 어려우며, 오류가 발생하기 쉽습니다. 문자열을 연결하다 보면 구분자를 잊기 쉽고, /를 하드코딩하면 Windows에서 스크립트가 깨집니다. 파일의 stem 이름을 얻기 위해 다섯 개의 os.path 호출을 연쇄적으로 사용하면, 3개월 후 아무도 그 코드를 읽을 수 없게 됩니다 -- 당신 포함해서요.

이것들은 예외적인 경우가 아닙니다. 모든 데이터 과학 파이프라인, 모든 웹 애플리케이션, 모든 자동화 스크립트는 파일 시스템에 접근합니다. 당신의 Mac에서는 작동하는 경로가 동료의 Windows 노트북에서는 실패합니다. 임시 경로 변수가 기술 부채처럼 코드에 누적됩니다. 그리고 os.path.join(os.path.dirname(...)) 호출을 더 많이 중첩할수록, 프로덕션에서만 표면화되는 미묘한 버그를 도입할 가능성이 높아집니다.

Python의 pathlib 모듈이 이 문제를 해결합니다. Python 3.4에 도입되었고 Python 3.6부터 완전히 성숙한 pathlib은 문자열 기반 경로 조작을 적절한 Path 객체로 대체합니다. 경로는 / 연산자로 결합됩니다. .name, .suffix, .stem과 같은 파일 속성은 함수 호출이 아닌 프로퍼티입니다. 파일 읽기와 쓰기는 한 줄로 끝납니다. 그리고 모든 것이 운영 체제 전반에서 동일하게 작동합니다. 이 가이드는 기본적인 경로 구성부터 데이터 과학 워크플로우를 위한 고급 패턴까지 모든 필수적인 pathlib 기능을 다룹니다.

📚

왜 pathlib인가: os.path 대신

pathlib이 등장하기 전에 Python 개발자들은 경로 작업을 위해 os.path에 의존했고 파일 시스템 상호작용을 위해 os를 사용했습니다. 그 접근법도 작동하지만, 경로를 일반 문자열로 취급합니다. 이것은 세 가지 지속적인 문제를 만듭니다:

  1. 가독성이 빠르게 저하됩니다. os.path.splitext(os.path.basename(filepath))[0]Path(filepath).stem을 비교해 보세요. 둘 다 확장자를 제외한 파일명을 추출합니다. 하나는 자체 문서화되어 있고, 다른 하나는 정신적으로 파싱해야 합니다.

  2. 크로스 플랫폼 버그. /를 구분자로 하드코딩하거나 문자열 연결을 사용하면 Linux 스크립트가 Windows에서 조용히 깨집니다. os.path.join은 도움이 되지만, 한 번이라도 사용하는 것을 잊으면 잠재적 버그가 생성됩니다.

  3. 흩어진 기능성. 경로를 작업하려면 분해를 위해 os.path, 디렉토리 생성을 위해 os, 패턴 매칭을 위해 glob, 파일 I/O를 위해 open()이 필요했습니다. pathlib은 이 모든 것을 단일 Path 객체로 통합합니다.

여기 동일한 작업 -- 데이터 디렉토리에서 모든 .csv 파일을 찾고 첫 번째 파일을 읽기 --을 두 스타일로 보여드립니다:

# os.path 접근법
import os
import glob
 
data_dir = os.path.join(os.path.expanduser('~'), 'projects', 'data')
csv_files = glob.glob(os.path.join(data_dir, '**', '*.csv'), recursive=True)
if csv_files:
    with open(csv_files[0], 'r') as f:
        content = f.read()
# pathlib 접근법
from pathlib import Path
 
data_dir = Path.home() / 'projects' / 'data'
csv_files = list(data_dir.rglob('*.csv'))
if csv_files:
    content = csv_files[0].read_text()

pathlib 버전은 더 짧고, 읽기 쉬우며, 정확히 동일한 작업을 수행합니다. Path 이외의 import는 필요 없습니다. 문자열 연결이 없습니다. 별도의 open() 호출이 없습니다.

Path 객체 생성하기

모든 pathlib 작업은 Path 객체를 생성하는 것부터 시작합니다. Path 클래스는 Linux/macOS에서는 자동으로 PosixPath를, Windows에서는 WindowsPath를 반환합니다.

from pathlib import Path
 
# 문자열에서
p = Path('/home/user/documents/report.csv')
 
# 여러 세그먼트에서 (자동으로 결합됨)
p = Path('home', 'user', 'documents', 'report.csv')
 
# 현재 작업 디렉토리
cwd = Path.cwd()
print(cwd)  # 예: /home/user/projects/myapp
 
# 사용자 홈 디렉토리
home = Path.home()
print(home)  # 예: /home/user
 
# 상대 경로
p = Path('data/output/results.csv')
 
# 기존 경로에서
base = Path('/home/user')
full = Path(base, 'documents', 'file.txt')
print(full)  # /home/user/documents/file.txt

인자 없이 Path()를 호출하면 Path('.')를 반환하며, 이는 현재 디렉토리에 대한 상대 경로입니다. 절대적인 현재 디렉토리가 필요할 때는 Path.cwd()를 사용하세요.

/ 연산자로 경로 결합하기

pathlib의 가장 특징적인 기능은 오버로드된 / 연산자입니다. os.path.join() 대신 /로 경로 세그먼트를 연결합니다:

from pathlib import Path
 
# 자연스럽게 경로 구축
project = Path.home() / 'projects' / 'analysis'
data_file = project / 'data' / 'sales_2026.csv'
print(data_file)  # /home/user/projects/analysis/data/sales_2026.csv
 
# Path 객체와 문자열 혼합
base = Path('/var/log')
app_log = base / 'myapp' / 'error.log'
print(app_log)  # /var/log/myapp/error.log
 
# 변수와 결합
filename = 'report.pdf'
output = Path('output') / filename
print(output)  # output/report.pdf

/ 연산자는 구분자를 자동으로 처리합니다. Windows에서 Path('C:/Users') / 'data'C:\Users\data를 생성합니다. 더 이상 /\를 고민할 필요가 없습니다.

joinpath()를 사용하여 동일한 결과를 얻을 수도 있습니다:

from pathlib import Path
 
# Path('data') / 'raw' / 'file.csv'와 동일
p = Path('data').joinpath('raw', 'file.csv')
print(p)  # data/raw/file.csv

경로 구성 요소

모든 Path 객체는 구성 요소를 프로퍼티로 노출합니다. 함수 호출도, 문자열 분할도 필요 없습니다.

from pathlib import Path
 
p = Path('/home/user/projects/analysis/data/sales_report.final.csv')
 
print(p.name)       # sales_report.final.csv  (확장자 포함 파일명)
print(p.stem)       # sales_report.final      (마지막 확장자 제외 파일명)
print(p.suffix)     # .csv                    (마지막 확장자)
print(p.suffixes)   # ['.final', '.csv']      (모든 확장자)
print(p.parent)     # /home/user/projects/analysis/data
print(p.anchor)     # /                       (Unix의 루트, Windows의 C:\)
print(p.parts)      # ('/', 'home', 'user', 'projects', 'analysis', 'data', 'sales_report.final.csv')

상위 디렉토리 탐색

.parent 프로퍼티는 직계 상위 디렉토리를 반환합니다. 더 높은 상위로 가려면 체인을 사용하세요:

from pathlib import Path
 
p = Path('/home/user/projects/analysis/data/output.csv')
 
print(p.parent)            # /home/user/projects/analysis/data
print(p.parent.parent)     # /home/user/projects/analysis
print(p.parent.parent.parent)  # /home/user/projects
 
# .parents는 모든 조상에 대한 인덱스 접근을 제공합니다
print(p.parents[0])  # /home/user/projects/analysis/data
print(p.parents[1])  # /home/user/projects/analysis
print(p.parents[2])  # /home/user/projects
print(p.parents[3])  # /home/user

경로 구성 요소 변경

.with_name(), .with_stem(), .with_suffix()를 사용하여 수정된 구성 요소로 새 경로를 생성합니다:

from pathlib import Path
 
p = Path('/data/reports/sales_q1.csv')
 
# 파일명 전체 변경
print(p.with_name('revenue_q1.csv'))    # /data/reports/revenue_q1.csv
 
# stem만 변경 (Python 3.9+)
print(p.with_stem('sales_q2'))          # /data/reports/sales_q2.csv
 
# 확장자만 변경
print(p.with_suffix('.parquet'))        # /data/reports/sales_q1.parquet
 
# 확장자 제거
print(p.with_suffix(''))                # /data/reports/sales_q1
 
# 확장자 추가
backup = p.with_suffix(p.suffix + '.bak')
print(backup)                           # /data/reports/sales_q1.csv.bak

이 메서드들은 새로운 Path 객체를 반환합니다. 디스크의 파일 이름을 바꾸지는 않습니다.

파일 I/O: 읽기와 쓰기

pathlib은 간단한 파일 작업을 위한 open() / with 보일러플레이트를 제거합니다:

from pathlib import Path
 
file_path = Path('example.txt')
 
# 파일에 텍스트 쓰기 (없으면 생성, 있으면 덮어쓰기)
file_path.write_text('Hello, pathlib!\nSecond line.')
 
# 파일에서 텍스트 읽기
content = file_path.read_text()
print(content)
# Hello, pathlib!
# Second line.
 
# 바이트 쓰기
binary_path = Path('data.bin')
binary_path.write_bytes(b'\x00\x01\x02\x03')
 
# 바이트 읽기
raw = binary_path.read_bytes()
print(raw)  # b'\x00\x01\x02\x03'

비ASCII 텍스트 작업 시 인코딩을 명시적으로 지정하세요:

from pathlib import Path
 
# UTF-8 텍스트 쓰기
Path('greeting.txt').write_text('こんにちは世界', encoding='utf-8')
 
# 인코딩과 함께 읽기
text = Path('greeting.txt').read_text(encoding='utf-8')
print(text)  # こんにちは世界

큰 파일이나 스트리밍 작업의 경우, 내장 open()과 마찬가지로 파일 핸들을 반환하는 .open()을 사용하세요:

from pathlib import Path
 
log_file = Path('application.log')
 
# 한 줄씩 쓰기
with log_file.open('w') as f:
    for i in range(1000):
        f.write(f'Event {i}: processed\n')
 
# 한 줄씩 읽기 (큰 파일에 메모리 효율적)
with log_file.open('r') as f:
    for line in f:
        if 'error' in line.lower():
            print(line.strip())

디렉토리 작업

디렉토리 생성

from pathlib import Path
 
# 단일 디렉토리 생성
Path('output').mkdir()
 
# 상위 디렉토리와 함께 생성 (os.makedirs와 유사)
Path('data/raw/2026/february').mkdir(parents=True, exist_ok=True)
 
# parents=True는 누락된 모든 상위 디렉토리를 생성합니다
# exist_ok=True는 디렉토리가 이미 존재할 때 오류를 방지합니다

흔한 실수는 parents=True를 잊는 것입니다. 없으면 상위 디렉토리가 하나라도 없으면 mkdir()FileNotFoundError를 발생시킵니다. 중첩된 디렉토리를 생성할 때는 항상 parents=True를 사용하고, 작업이 멱등성을 갖도록 exist_ok=True를 사용하세요.

디렉토리 내용 나열

from pathlib import Path
 
project = Path('.')
 
# 모든 항목 나열 (파일과 디렉토리)
for entry in project.iterdir():
    print(entry.name, '(dir)' if entry.is_dir() else '(file)')
 
# 파일만 필터링
files = [f for f in project.iterdir() if f.is_file()]
print(f"Found {len(files)} files")
 
# 디렉토리만 필터링
dirs = [d for d in project.iterdir() if d.is_dir()]
print(f"Found {len(dirs)} directories")
 
# 이름으로 정렬
for entry in sorted(project.iterdir()):
    print(entry.name)

디렉토리와 파일 삭제

from pathlib import Path
 
# 파일 삭제
Path('temp_output.csv').unlink()
 
# 파일이 존재할 때만 삭제 (Python 3.8+)
Path('temp_output.csv').unlink(missing_ok=True)
 
# 빈 디렉토리 삭제
Path('empty_dir').rmdir()

rmdir()은 빈 디렉토리만 삭제합니다. 비어있지 않은 디렉토리의 경우 shutil.rmtree()를 사용하세요:

from pathlib import Path
import shutil
 
target = Path('data/old_output')
if target.exists():
    shutil.rmtree(target)

Glob 패턴: 파일 찾기

pathlib에는 내장 glob 지원이 있습니다. glob 모듈을 별도로 import할 필요가 없습니다.

기본 Glob

from pathlib import Path
 
project = Path('/home/user/project')
 
# 디렉토리의 모든 Python 파일 찾기
for py_file in project.glob('*.py'):
    print(py_file.name)
 
# 모든 CSV 파일 찾기
csv_files = list(project.glob('*.csv'))
print(f"Found {len(csv_files)} CSV files")
 
# 패턴과 일치하는 파일 찾기
reports = list(project.glob('report_*.xlsx'))

rglob으로 재귀적 Glob

rglob()은 모든 하위 디렉토리를 재귀적으로 검색합니다. glob('**/*.pattern')과 동일하지만 더 편리합니다:

from pathlib import Path
 
project = Path('/home/user/project')
 
# 모든 하위 디렉토리의 모든 Python 파일 찾기
all_py = list(project.rglob('*.py'))
print(f"Found {len(all_py)} Python files across all directories")
 
# 재귀적으로 모든 Jupyter 노트북 찾기
notebooks = list(project.rglob('*.ipynb'))
for nb in notebooks:
    print(f"  {nb.relative_to(project)}")
 
# 모든 이미지 파일 찾기
images = list(project.rglob('*.png')) + list(project.rglob('*.jpg'))
 
# 모든 파일 찾기 (필터 없음)
all_files = [f for f in project.rglob('*') if f.is_file()]

고급 Glob 패턴

from pathlib import Path
 
data = Path('data')
 
# 단일 문자 와일드카드
data.glob('file_?.csv')        # file_1.csv, file_a.csv
 
# 문자 범위
data.glob('report_202[456].csv')  # report_2024.csv, report_2025.csv, report_2026.csv
 
# 모든 하위 디렉토리 레벨
data.glob('**/output/*.csv')   # data/raw/output/result.csv, data/processed/output/result.csv
 
# 여러 확장자 (두 개의 glob 결합)
from itertools import chain
all_data = chain(data.rglob('*.csv'), data.rglob('*.parquet'))

경로 확인

pathlib은 경로 상태를 확인하는 명확한 boolean 메서드를 제공합니다:

from pathlib import Path
 
p = Path('/home/user/projects/data.csv')
 
# 경로가 존재합니까?
print(p.exists())       # True 또는 False
 
# 파일입니까?
print(p.is_file())      # 존재하고 일반 파일이면 True
 
# 디렉토리입니까?
print(p.is_dir())       # 존재하고 디렉토리면 True
 
# 심볼릭 링크입니까?
print(p.is_symlink())   # 존재하고 심볼릭 링크면 True
 
# 절대 경로입니까?
print(p.is_absolute())  # True (/home/...이 루트로 시작)
print(Path('data.csv').is_absolute())  # False (상대 경로)

이 메서드들은 존재하지 않는 경로에서도 예외를 발생시키지 않습니다. 단순히 False를 반환하므로 조건문에서 안전하게 사용할 수 있습니다:

from pathlib import Path
 
config = Path('config.yaml')
if config.is_file():
    settings = config.read_text()
else:
    print("Config file not found, using defaults")

경로 조작

경로 해석 및 정규화

from pathlib import Path
 
# 절대 경로로 해석 (심볼릭 링크도 해석)
p = Path('data/../data/./output.csv')
print(p.resolve())  # /home/user/project/data/output.csv
 
# 심볼릭 링크를 해석하지 않고 절대 경로 얻기
print(p.absolute())  # /home/user/project/data/../data/./output.csv
 
# 사용자 홈 디렉토리 확장
p = Path('~/Documents/report.csv')
print(p.expanduser())  # /home/user/Documents/report.csv

상대 경로

from pathlib import Path
 
full_path = Path('/home/user/projects/analysis/data/output.csv')
base = Path('/home/user/projects')
 
# base에서 full_path까지의 상대 경로 얻기
relative = full_path.relative_to(base)
print(relative)  # analysis/data/output.csv
 
# 경로가 base에 대해 상대적이지 않으면 ValueError 발생
try:
    Path('/var/log/app.log').relative_to(base)
except ValueError as e:
    print(e)  # '/var/log/app.log' is not relative to '/home/user/projects'
 
# Python 3.12+: is_relative_to() 확인
print(full_path.is_relative_to(base))   # True
print(Path('/var/log').is_relative_to(base))  # False

파일 메타데이터와 Stat

from pathlib import Path
from datetime import datetime
 
p = Path('data.csv')
 
# 파일 상태 얻기
stat = p.stat()
print(f"Size: {stat.st_size} bytes")
print(f"Modified: {datetime.fromtimestamp(stat.st_mtime)}")
print(f"Created: {datetime.fromtimestamp(stat.st_ctime)}")
 
# 편의 기능: stat를 통해 크기 직접 얻기
size_mb = p.stat().st_size / (1024 * 1024)
print(f"Size: {size_mb:.2f} MB")
 
# 두 경로가 동일한 파일을 가리키는지 확인
p1 = Path('/home/user/data.csv')
p2 = Path.home() / 'data.csv'
print(p1.samefile(p2))  # True (동일한 파일로 해석되면)

파일 이름 변경 및 이동

from pathlib import Path
 
# 파일 이름 변경 (새 Path 반환)
old = Path('report_draft.csv')
new = old.rename('report_final.csv')
print(new)  # report_final.csv
 
# 다른 디렉토리로 이동
source = Path('output/temp_results.csv')
dest = source.rename(Path('archive') / source.name)
 
# 파일 교체 (대상이 존재하면 덮어쓰기)
Path('new_data.csv').replace('data.csv')

참고: .rename()은 Unix에서는 대상 파일을 덮어쓰지만 Windows에서는 오류를 발생시킬 수 있습니다. 보장된 크로스 플랫폼 덮어쓰기 동작을 위해서는 .replace()를 사용하세요.

os.path vs pathlib: 완전 비교

여기 흔한 os.path 작업을 pathlib 등가물에 매핑하는 참조 표가 있습니다:

작업os.path / ospathlib
경로 결합os.path.join('a', 'b')Path('a') / 'b'
현재 디렉토리os.getcwd()Path.cwd()
홈 디렉토리os.path.expanduser('~')Path.home()
절대 경로os.path.abspath(p)Path(p).resolve()
파일명os.path.basename(p)Path(p).name
디렉토리os.path.dirname(p)Path(p).parent
확장자os.path.splitext(p)[1]Path(p).suffix
Stem (확장자 제외 이름)os.path.splitext(os.path.basename(p))[0]Path(p).stem
존재 여부os.path.exists(p)Path(p).exists()
파일 여부os.path.isfile(p)Path(p).is_file()
디렉토리 여부os.path.isdir(p)Path(p).is_dir()
심볼릭 링크 여부os.path.islink(p)Path(p).is_symlink()
절대 경로 여부os.path.isabs(p)Path(p).is_absolute()
파일 크기os.path.getsize(p)Path(p).stat().st_size
디렉토리 나열os.listdir(p)Path(p).iterdir()
디렉토리 생성os.makedirs(p, exist_ok=True)Path(p).mkdir(parents=True, exist_ok=True)
파일 삭제os.remove(p)Path(p).unlink()
디렉토리 삭제os.rmdir(p)Path(p).rmdir()
이름 변경os.rename(old, new)Path(old).rename(new)
파일 읽기open(p).read()Path(p).read_text()
파일 쓰기open(p, 'w').write(text)Path(p).write_text(text)
Globglob.glob('*.py')Path('.').glob('*.py')
재귀적 Globglob.glob('**/*.py', recursive=True)Path('.').rglob('*.py')
사용자 확장os.path.expanduser(p)Path(p).expanduser()
상대 경로os.path.relpath(p, base)Path(p).relative_to(base)

임시 파일 작업

pathlib은 Python의 tempfile 모듈과 깔끔하게 통합됩니다:

from pathlib import Path
import tempfile
 
# Path로 임시 디렉토리 생성
with tempfile.TemporaryDirectory() as tmp_dir:
    tmp_path = Path(tmp_dir)
 
    # pathlib을 사용해 임시 파일 쓰기
    data_file = tmp_path / 'intermediate_results.csv'
    data_file.write_text('col1,col2\n1,2\n3,4\n')
 
    config_file = tmp_path / 'run_config.json'
    config_file.write_text('{"epochs": 100, "lr": 0.001}')
 
    # 생성한 것들 나열
    for f in tmp_path.iterdir():
        print(f"{f.name}: {f.stat().st_size} bytes")
 
    # 파일 처리...
    print(data_file.read_text())
 
# 여기서 디렉토리와 모든 파일이 자동으로 삭제됨
from pathlib import Path
import tempfile
 
# 이름이 있는 임시 파일 생성
tmp = tempfile.NamedTemporaryFile(suffix='.csv', delete=False)
tmp_path = Path(tmp.name)
tmp.close()
 
# pathlib을 사용해 쓰기
tmp_path.write_text('id,value\n1,100\n2,200\n')
print(f"Temp file at: {tmp_path}")
 
# 완료 후 정리
tmp_path.unlink()

데이터 과학 워크플로우에서의 Pathlib

데이터 과학 프로젝트는 일반적으로 여러 디렉토리에서 데이터셋을 읽고, 결과를 위한 출력 폴더를 생성하며, 실험 아티팩트를 관리합니다. pathlib은 이러한 패턴을 깔끔하고 신뢰할 수 있게 만듭니다.

프로젝트 디렉토리 구성

from pathlib import Path
 
def setup_experiment(experiment_name):
    """표준 실험 디렉토리 구조 생성."""
    base = Path('experiments') / experiment_name
 
    dirs = ['data/raw', 'data/processed', 'models', 'results/figures', 'results/tables', 'logs']
 
    for d in dirs:
        (base / d).mkdir(parents=True, exist_ok=True)
 
    # 설정 파일 생성
    config = base / 'config.json'
    if not config.exists():
        config.write_text('{"learning_rate": 0.001, "epochs": 50}')
 
    print(f"Experiment directory ready: {base.resolve()}")
    return base
 
project = setup_experiment('sales_forecast_v2')

여러 데이터 파일 읽기

from pathlib import Path
import pandas as pd
 
data_dir = Path('data/raw')
 
# 모든 CSV 파일을 단일 DataFrame으로 읽기
dfs = []
for csv_file in sorted(data_dir.glob('*.csv')):
    print(f"Loading {csv_file.name}...")
    df = pd.read_csv(csv_file)
    df['source_file'] = csv_file.stem  # 소스 파일명 추가
    dfs.append(df)
 
combined = pd.concat(dfs, ignore_index=True)
print(f"Loaded {len(combined)} rows from {len(dfs)} files")
 
# 처리된 디렉토리에 저장
output_path = Path('data/processed') / 'combined_sales.parquet'
output_path.parent.mkdir(parents=True, exist_ok=True)
combined.to_parquet(output_path)

pathlib로 CSV 데이터를 로드한 후, PyGWalker (opens in a new tab)로 시각적으로 탐색할 수 있습니다. PyGWalker는 모든 Pandas DataFrame을 Tableau와 같은 인터랙티브 인터페이스로 변환하여 드래그 앤 드롭 데이터 탐색을 가능하게 합니다 -- 추가 코드 없이.

실험 결과 저장

from pathlib import Path
from datetime import datetime
import json
 
def save_results(metrics, experiment_dir):
    """타임스탬프와 함께 실험 메트릭 저장."""
    results_dir = Path(experiment_dir) / 'results'
    results_dir.mkdir(parents=True, exist_ok=True)
 
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    output_file = results_dir / f'metrics_{timestamp}.json'
 
    output_file.write_text(json.dumps(metrics, indent=2))
    print(f"Results saved to {output_file}")
    return output_file
 
# 사용법
metrics = {'accuracy': 0.94, 'f1_score': 0.91, 'loss': 0.187}
save_results(metrics, 'experiments/sales_forecast_v2')

노트북에서 파일 경로 관리

Jupyter 노트북에서 작업할 때, 노트북의 작업 디렉토리가 프로젝트 루트와 다를 수 있어 경로가 종종 깨집니다. pathlib은 이를 쉽게 처리합니다:

from pathlib import Path
 
# 노트북 위치에서 항상 절대 경로로 해석
NOTEBOOK_DIR = Path.cwd()
PROJECT_ROOT = NOTEBOOK_DIR.parent  # 노트북이 notebooks/에 있을 경우
DATA_DIR = PROJECT_ROOT / 'data'
OUTPUT_DIR = PROJECT_ROOT / 'output'
 
# 이제 모든 경로가 절대적이고 신뢰할 수 있음
train_data = DATA_DIR / 'train.csv'
print(f"Loading: {train_data}")
assert train_data.exists(), f"Missing: {train_data}"

Jupyter에서 광범위하게 작업하고 프로젝트 파일과 데이터 경로를 관리하는 데 도움이 되는 AI 기반 환경을 원한다면, RunCell (opens in a new tab)은 노트북에 AI 에이전트 레이어를 추가합니다. 필요한 것을 설명하세요 -- "data 디렉토리에서 모든 Parquet 파일을 찾아 가장 최신 것을 로드해줘" -- 그러면 pathlib 코드를 생성하고 실행해줍니다.

일반적인 패턴과 레시피

원자적 교체를 통한 안전한 파일 쓰기

임시 파일에 먼저 쓰고 원자적으로 대상을 교체하여 데이터 손상을 방지합니다:

from pathlib import Path
import tempfile
 
def safe_write(target_path, content):
    """손상을 방지하기 위해 원자적으로 파일에 내용 쓰기."""
    target = Path(target_path)
    target.parent.mkdir(parents=True, exist_ok=True)
 
    # 동일한 디렉토리에 임시 파일로 쓰기
    tmp = tempfile.NamedTemporaryFile(
        mode='w', dir=target.parent, suffix='.tmp', delete=False
    )
    tmp_path = Path(tmp.name)
    try:
        tmp.write(content)
        tmp.close()
        tmp_path.replace(target)  # 대부분의 파일 시스템에서 원자적
    except Exception:
        tmp_path.unlink(missing_ok=True)
        raise
 
safe_write('config/settings.json', '{"debug": true}')

일괄 파일 이름 변경

from pathlib import Path
 
photos_dir = Path('photos')
 
# 모든 .jpeg 파일을 .jpg로 이름 변경
for f in photos_dir.glob('*.jpeg'):
    f.rename(f.with_suffix('.jpg'))
 
# 모든 파일에 접두사 추가
for i, f in enumerate(sorted(photos_dir.glob('*.jpg')), start=1):
    new_name = f.parent / f'photo_{i:04d}{f.suffix}'
    f.rename(new_name)

크기로 중복 파일 찾기

from pathlib import Path
from collections import defaultdict
 
def find_potential_duplicates(directory):
    """동일한 크기의 파일 찾기 (잠재적 중복)."""
    size_map = defaultdict(list)
 
    for f in Path(directory).rglob('*'):
        if f.is_file():
            size_map[f.stat().st_size].append(f)
 
    # 두 개 이상의 파일이 있는 그룹만 반환
    return {size: files for size, files in size_map.items() if len(files) > 1}
 
dupes = find_potential_duplicates('data')
for size, files in dupes.items():
    print(f"\n{size} bytes:")
    for f in files:
        print(f"  {f}")

파일 트리 시각화 구축

from pathlib import Path
 
def tree(directory, prefix='', max_depth=3, _depth=0):
    """디렉토리의 트리 구조 출력."""
    if _depth >= max_depth:
        return
 
    path = Path(directory)
    entries = sorted(path.iterdir(), key=lambda e: (e.is_file(), e.name))
 
    for i, entry in enumerate(entries):
        is_last = (i == len(entries) - 1)
        connector = '└── ' if is_last else '├── '
        print(f'{prefix}{connector}{entry.name}')
 
        if entry.is_dir():
            extension = '    ' if is_last else '│   '
            tree(entry, prefix + extension, max_depth, _depth + 1)
 
tree('my_project', max_depth=3)

출력:

├── data
│   ├── processed
│   │   └── combined.csv
│   └── raw
│       ├── sales_2025.csv
│       └── sales_2026.csv
├── notebooks
│   └── analysis.ipynb
├── output
│   └── figures
└── requirements.txt

흔한 실수와 피하는 방법

실수 1: 문자열과 Path 객체 비교

from pathlib import Path
 
p = Path('data/output.csv')
 
# 잘못됨: 문자열과 Path 비교
if p == 'data/output.csv':  # 작동할 수 있지만 취약함
    print("Match")
 
# 올바름: Path와 Path 비교, 또는 str() 사용
if p == Path('data/output.csv'):
    print("Match")
 
# 올바름: 필요시 문자열로 변환
if str(p) == 'data/output.csv':
    print("Match")

실수 2: mkdir에서 parents=True 잊기

from pathlib import Path
 
# 잘못됨: 'data'가 없으면 FileNotFoundError 발생
# Path('data/raw/2026').mkdir()
 
# 올바름: 누락된 모든 상위 생성
Path('data/raw/2026').mkdir(parents=True, exist_ok=True)

실수 3: / 대신 문자열 연결 사용

from pathlib import Path
 
base = Path('/home/user')
 
# 잘못됨: 문자열 연결은 pathlib을 망칩니다
# bad = base + '/data/file.csv'  # TypeError
 
# 올바름: / 연산자 사용
good = base / 'data' / 'file.csv'

실수 4: 문자열을 기대하는 라이브러리에 Path 전달

대부분의 현대적인 라이브러리(Pandas, NumPy, PIL 등)는 Path 객체를 기본적으로 수용합니다. 하지만 문자열이 필요한 이전 라이브러리를 만나면 명시적으로 변환하세요:

from pathlib import Path
 
p = Path('data/output.csv')
 
# 대부분의 라이브러리는 Path를 직접 수용
import pandas as pd
df = pd.read_csv(p)  # 잘 작동함
 
# 문자열이 필요한 이전 라이브러리의 경우
import some_legacy_lib
some_legacy_lib.process(str(p))  # str()으로 변환
 
# os.fspath()도 작동 (Python 3.6+)
import os
some_legacy_lib.process(os.fspath(p))

실수 5: 하드코딩된 경로 사용

from pathlib import Path
 
# 잘못됨: 하드코딩된 절대 경로
# data_path = Path('/home/alice/project/data/sales.csv')
 
# 올바름: 상대적 또는 동적 구성 요소에서 구축
data_path = Path.cwd() / 'data' / 'sales.csv'
 
# 올바름: 홈 디렉토리에서 구축
config_path = Path.home() / '.config' / 'myapp' / 'settings.json'
 
# 올바름: 환경 변수에서 구축
import os
data_root = Path(os.getenv('DATA_DIR', 'data'))
data_path = data_root / 'sales.csv'

자주 묻는 질문

Python에서 pathlib이란?

pathlib은 Python 3.4에 도입된 표준 라이브러리 모듈로, 파일 시스템 경로 작업을 위한 객체 지향 클래스를 제공합니다. os.path.join()과 같은 함수를 사용하는 대신 Path 객체를 생성하고 메서드와 연산자를 사용합니다. 크로스 플랫폼 경로 차이를 자동으로 처리합니다.

언제 pathlib을 os.path 대신 사용해야 하나요?

Python 3.6+의 모든 새 프로젝트에서 pathlib을 사용하세요. 더 깔끅하고 읽기 쉬운 코드를 생성하고, 경로 작업을 단일 객체로 통합하며, 크로스 플랫폼 문제를 자동으로 처리합니다. os.path를 사용해야 하는 유일한 이유는 Python 2를 지원해야 하는 레거시 코드를 유지하거나, os.environ과 같이 pathlib 등가물이 없는 소수의 os 함수를 사용하는 경우입니다.

pathlib은 Windows에서 작동하나요?

네. pathlib은 Windows에서는 자동으로 WindowsPath 객체를 사용하고 Linux/macOS에서는 PosixPath를 사용합니다. / 연산자는 Windows에서 백슬래시로 구분된 경로를 생성합니다. 모든 플랫폼에서 동일한 코드를 작성하면 pathlib이 차이를 처리합니다.

Path 객체를 Pandas와 함께 사용할 수 있나요?

네. Python 3.6과 Pandas 0.21+부터 Path 객체를 pd.read_csv(), pd.read_excel(), df.to_csv() 및 기타 I/O 함수에 직접 전달할 수 있습니다. str() 변환이 필요 없습니다.

Path.resolve()와 Path.absolute()의 차이점은?

.resolve()는 절대 경로를 반환하고 모든 심볼릭 링크와 ../. 구성 요소도 해석합니다. .absolute()는 심볼릭 링크를 해석하거나 경로를 정규화하지 않고 절대 경로를 반환합니다. 대부분의 경우 .resolve()가 필요한 것입니다.

Path 객체와 문자열 간에 어떻게 변환하나요?

str(path)를 사용하여 Path를 문자열로 변환하세요. Path(string)을 사용하여 문자열에서 Path를 생성하세요. os.fspath(path)도 명시적인 문자열 변환에 사용할 수 있습니다. 대부분의 현대적인 Python 라이브러리는 Path 객체를 직접 수용하므로 변환은 거의 필요 없습니다.

결론

Python의 pathlib 모듈은 파일 경로 조작을 위한 현대적인 표준입니다. / 연산자는 경로 결합을 읽기 쉽게 만듭니다. .name, .stem, .suffix, .parent와 같은 프로퍼티는 장황한 os.path 함수 체인을 제거합니다. 읽기, 쓰기, 디렉토리 생성, globbing을 위한 내장 메서드는 이전에 os, os.path, glob, open()이 필요했던 것을 단일하고 일관된 API로 통합합니다.

os.path에서 pathlib로의 마이그레이션은 간단합니다: os.path.join()/로, os.path.exists().exists()로, os.makedirs().mkdir(parents=True)로, glob.glob().glob() 또는 .rglob()로 교체하세요. Pandas, NumPy, PIL, PyTorch를 포함한 모든 주요 Python 라이브러리는 이제 Path 객체를 기본적으로 수용합니다. 새 프로젝트에서 이를 피할 이유가 없습니다.

작게 시작하세요. 지저분한 os.path 코드가 있는 스크립트 하나를 고르세요. 경로 작업을 pathlib로 교체하세요. 코드는 더 짧아지고, 더 읽기 쉬워지며, 더 이식 가능해질 것입니다. 그리고 다음 스크립트에도 동일하게 적용하세요.

📚