상품명, 수량, 가격을 리스트로 만들었는데 프로그램을 종료한 뒤에도 표 형태로 남기고 싶을 때가 있습니다. 쉼표를 직접 붙여 문자열을 만들 수도 있지만, 상품명 자체에 쉼표가 들어가면 어느 쉼표가 열을 나누는 기호인지 구분하기 어려워집니다. 이럴 때 파이썬의 csv 모듈을 사용하면 리스트의 각 값을 CSV 한 행으로 저장하고 다시 읽을 수 있습니다.
앞의 파일 읽기와 쓰기 글에서는 문자열을 파일에 저장하는 기본 흐름을 다뤘습니다. JSON 저장과 읽기 글는 키와 값을 가진 설정 데이터를 보관했습니다. 이번 글은 여러 주문처럼 같은 열이 반복되는 표 데이터를 다룹니다.
확인 기준 : 2026년 9월 23일. Windows와 Python 3.9.13에서 아래 예제 여섯 개를 각각 실행하고, 빈 파일을 읽는 경우도 추가로 확인했습니다. Python 3.14 공식 문서에서도 csv.reader(), csv.writer(), newline 동작을 확인했습니다. 추가 패키지는 설치하지 않습니다.

AI로 제작한 CSV 저장과 읽기 개념 일러스트입니다.
CSV는 같은 항목이 반복되는 표 데이터에 어울립니다
CSV는 한 행의 값을 구분 기호로 나누어 저장하는 텍스트 형식입니다. 이름의 원래 뜻은 Comma-Separated Values이며, 흔히 쉼표를 구분 기호로 사용합니다. 첫 행에는 상품,수량,가격처럼 열 이름을 적고, 그 아래에 같은 순서로 데이터를 이어갈 수 있습니다.
파이썬에 기본으로 포함된 csv 모듈은 표 데이터를 CSV 규칙에 맞게 읽고 쓰는 기능 모음입니다. 그 안의 csv.writer()는 쓰기용 파일을 입력받아 행을 기록하는 writer 객체를 돌려주는 함수입니다. csv.reader()는 읽기용 파일을 입력받아 한 행씩 리스트로 꺼낼 수 있는 reader 객체를 돌려주는 함수입니다. 리스트에 붙여 쓰는 메서드가 아니라 모듈 이름과 함께 호출합니다.
입력, 처리, 출력을 먼저 나누어 보면 다음과 같습니다.
| 기능 | 입력 | 처리 | 결과 |
|---|---|---|---|
csv.writer(file) |
쓰기용 파일 | 파일에 쓸 준비를 함. 아직 행을 쓰지는 않음 | 행을 쓸 writer 객체 |
writer.writerow(row) |
한 행을 나타내는 리스트 | 리스트 원소를 CSV 한 행으로 변환 | 열린 파일의 내용이 바뀜 |
csv.reader(file) |
읽기용 파일 | 파일에서 행을 읽을 준비를 함 | 행을 읽을 reader 객체 |
for row in reader |
reader 객체 | 다음 행을 순서대로 읽음 | 문자열로 이루어진 새 리스트 |
저장할 때 원본 리스트는 바뀌지 않고 파일 내용이 바뀝니다. 읽을 때는 파일을 수정하지 않고 각 행에 해당하는 새 리스트가 만들어집니다. 단, CSV는 숫자의 자료형까지 보존하는 형식이 아니므로 읽은 2와 4500도 기본적으로 문자열이 됩니다.
writer로 orders.csv를 만듭니다
빈 실습 폴더에 01_write_orders.py를 만들고 아래 코드를 저장합니다. 터미널도 그 실습 폴더에서 열어 실행하면 orders.csv가 같은 위치에 생깁니다. 상대경로의 기준은 파이썬 파일 위치가 아니라 터미널의 현재 작업 폴더입니다. 실행 위치가 헷갈린다면 FileNotFoundError와 상대경로 확인 방법을 먼저 확인하세요. 이미 같은 이름의 파일이 있다면 쓰기 모드 "w"가 기존 내용을 지우므로 별도 실습 폴더에서 진행하세요.
첫 코드에서 확인할 부분은 리스트 한 개가 CSV 한 행이 된다는 점입니다. writerow()에는 한 행을 넣고, writerows()에는 여러 행이 들어 있는 리스트를 넣습니다. 둘 다 writer 객체에 붙어 있는 기능인 메서드입니다. 마지막의 기본 함수 len(orders)는 리스트에 담긴 주문 개수를 돌려주고, print()는 그 값을 화면에 표시합니다.
import csv
orders = [
["아메리카노", 2, 4500],
["샌드위치, 햄치즈", 1, 6800],
["레몬에이드", 3, 5000],
]
with open("orders.csv", "w", encoding="utf-8", newline="") as file:
writer = csv.writer(file)
writer.writerow(["상품", "수량", "가격"])
writer.writerows(orders)
print("저장한 주문 수 :", len(orders))
import csv는 파이썬 표준 라이브러리의 CSV 기능을 불러옵니다. open()은 orders.csv를 UTF-8 문자 인코딩의 쓰기 모드로 열고, as file은 열린 파일을 코드 안에서 file이라는 이름으로 사용하게 합니다. with 블록을 벗어나면 파일이 닫힙니다.
핵심 줄 writer = csv.writer(file)은 오른쪽부터 읽습니다. 열린 파일을 csv.writer()에 넣어 writer 객체를 만든 뒤 왼쪽 변수에 저장합니다. 다음 줄의 writer.writerow([...])는 열 이름 한 행을 쓰고, writer.writerows(orders)는 orders 안의 세 리스트를 세 행으로 씁니다.
여기서 newline=""은 빈 문자열을 파일에 쓰라는 뜻이 아닙니다. 텍스트 파일 기능이 줄바꿈을 먼저 바꾸지 않고 CSV 모듈이 직접 처리하도록 맡기는 설정입니다. Python 공식 문서는 CSV 파일 객체를 열 때 newline=""을 사용하도록 안내합니다. 이를 생략하면 따옴표 안의 줄바꿈을 올바르게 읽지 못하거나, \r\n 줄바꿈을 사용하는 환경에서 쓸 때 \r이 하나 더 붙을 수 있습니다.
터미널을 실습 폴더에서 열고 실행합니다.
python 01_write_orders.py
직접 실행한 출력입니다.
저장한 주문 수 : 3
생성된 orders.csv를 텍스트 편집기로 열면 다음처럼 보입니다.
상품,수량,가격
아메리카노,2,4500
"샌드위치, 햄치즈",1,6800
레몬에이드,3,5000
두 번째 상품명에는 쉼표가 들어 있기 때문에 writer가 자동으로 큰따옴표를 붙였습니다. 이 따옴표 덕분에 샌드위치, 햄치즈가 두 열로 갈라지지 않고 상품명 하나로 보존됩니다. 원본 orders 리스트에는 큰따옴표가 추가되지 않습니다.
reader는 한 행을 문자열 리스트로 돌려줍니다
같은 실습 폴더에 02_read_orders.py를 만듭니다. 읽기 모드 "r"로 파일을 열고 csv.reader(file)에 넘깁니다. reader 객체는 파일 전체를 한꺼번에 보여 주는 리스트가 아니라, 반복할 때 다음 행을 차례대로 내주는 객체입니다.
import csv
with open("orders.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.reader(file)
header = next(reader)
print("열 이름 :", header)
for row in reader:
print(row)
next(reader)는 reader의 다음 행 하나를 꺼냅니다. 파일의 첫 행이 열 이름이므로 header에 따로 저장했습니다. 이때 reader의 위치도 다음 행으로 이동합니다. 이어지는 for문은 첫 주문부터 마지막 주문까지 남은 행만 읽습니다.
csv.reader(file)을 reader 변수에 저장했을 뿐인 시점에는 아직 출력할 행이 없습니다. next()나 for문이 다음 행을 요청할 때 CSV 내용을 해석해 리스트를 돌려줍니다. 이 과정은 파일 내용을 바꾸지 않습니다.
먼저 01_write_orders.py를 실행해 CSV 파일을 만든 뒤 실행합니다.
python 02_read_orders.py
실행 결과입니다.
열 이름 : ['상품', '수량', '가격']
['아메리카노', '2', '4500']
['샌드위치, 햄치즈', '1', '6800']
['레몬에이드', '3', '5000']
CSV 파일에서 큰따옴표로 감싸졌던 상품명도 리스트 원소 하나로 복원되었습니다. 결과에 보이는 작은따옴표는 파이썬이 문자열 리스트를 화면에 표현한 것이며 상품명에 저장된 문자가 아닙니다.
파일이 비어 있으면 next(reader)는 어떻게 될까요
위 코드는 첫 행이 있다고 가정합니다. 내용이 하나도 없는 파일에서 next(reader)를 실행하면 다음 행이 없다는 뜻의 StopIteration 오류가 납니다. 두 번째 인수로 None을 넣으면 행이 없을 때 오류 대신 그 값을 받습니다. None은 여기서 읽을 행이 없음을 나타내는 값입니다.
다음 코드는 앞에서 만든 orders.csv를 읽으면서 빈 파일도 구분하는 전체 예제입니다. 07_empty_file.py로 저장해 실행하세요.
import csv
with open("orders.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.reader(file)
header = next(reader, None)
if header is None:
print("CSV 파일에 읽을 행이 없습니다.")
else:
print("열 이름 :", header)
for row in reader:
print(row)
header is None은 읽은 행이 없는지를 확인하는 조건입니다. 원래 주문 파일로 실행하면 앞의 읽기 예제와 같은 결과가 나옵니다. 파일을 비운 복사본으로 검사했을 때는 다음 문장만 출력됐습니다.
CSV 파일에 읽을 행이 없습니다.
헤더만 있는 파일은 빈 파일과 다릅니다. 헤더는 출력되고, 이후 for문에서 꺼낼 주문만 없습니다. 또한 next(reader, None)이 열 이름을 찾아 주는 것은 아닙니다. 첫 행부터 실제 주문인 파일에 이 코드를 쓰면 첫 주문을 헤더로 소비하므로, 입력 파일에 헤더가 있는지 먼저 확인해야 합니다.
CSV에서 읽은 숫자는 바로 계산할 수 없습니다
출력에서 수량과 가격에도 작은따옴표가 붙었습니다. csv.reader()는 별도 변환 옵션을 사용하지 않으면 모든 필드를 문자열로 돌려줍니다. 파일에 2라고 적혀 있어도 파이썬 정수 2가 아니라 문자열 "2"입니다.
아래 03_number_type_error.py는 첫 주문의 수량에 숫자 1을 더하려다 실패하는 예제입니다.
import csv
with open("orders.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.reader(file)
next(reader)
first_order = next(reader)
quantity = first_order[1]
print("읽은 수량 :", quantity)
print("자료형 :", type(quantity).__name__)
print(quantity + 1)
마지막 줄에서 문자열과 정수를 더하려 했기 때문에 다음 오류가 납니다.
읽은 수량 : 2
자료형 : str
Traceback (most recent call last):
...
TypeError: can only concatenate str (not "int") to str
first_order[1]은 두 번째 원소인 수량 문자열입니다. type(quantity).__name__으로 자료형 이름을 확인하면 str이 출력됩니다. 계산이 필요한 열은 int(row[1])처럼 원하는 자료형으로 직접 바꿔야 합니다. 모든 열을 숫자로 바꾸는 것이 아니라, 어떤 열이 숫자인지 알고 있는 코드에서 변환합니다.
JSON은 따옴표 없는 숫자를 읽으면 파이썬 숫자로 복원하지만, CSV는 각 칸의 자료형 규칙을 따로 저장하지 않습니다. 표 모양이 같아 보여도 이 차이를 기억해 두면 됩니다.
수량과 가격을 int로 바꿔 전체 금액을 계산합니다
04_calculate_total.py에서는 헤더를 한 번 건너뛴 뒤 각 주문의 수량과 가격을 정수로 바꿉니다. row[0], row[1], row[2]는 각각 첫 번째, 두 번째, 세 번째 열입니다.
import csv
total = 0
with open("orders.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.reader(file)
next(reader)
for row in reader:
product = row[0]
quantity = int(row[1])
price = int(row[2])
subtotal = quantity * price
total += subtotal
print(f"{product} : {subtotal:,}원")
print(f"전체 금액 : {total:,}원")
오른쪽의 int(row[1])이 수량 문자열을 정수로 바꾸고 그 결과를 quantity에 저장합니다. 가격도 같은 순서로 처리합니다. 한 주문의 금액은 수량과 가격을 곱해 구하고, total += subtotal은 그 금액을 누적합니다.
실행 결과는 다음과 같습니다.
아메리카노 : 9,000원
샌드위치, 햄치즈 : 6,800원
레몬에이드 : 15,000원
전체 금액 : 30,800원
수량 칸이 비어 있거나 두 개처럼 숫자로 바꿀 수 없는 글자가 들어 있으면 int()에서 ValueError가 납니다. 외부에서 받은 CSV라면 변환 전에 빈 문자열인지 확인하거나, try except 예외 처리 글의 방식으로 어떤 행에서 실패했는지 알려 주는 처리가 필요합니다.
또한 행마다 열 개수가 같다는 보장이 없다면 row[2]를 사용하기 전에 len(row)를 확인해야 합니다. 세 번째 열이 없는 행에서 row[2]를 읽으면 IndexError가 납니다. CSV는 열 구조를 강제로 지켜 주는 데이터베이스가 아니므로, 다른 사람이 만든 파일을 읽을 때는 빈 칸과 열 개수를 함께 점검하세요.
쉼표와 줄바꿈이 든 값은 split보다 csv 모듈이 안전합니다
CSV를 단순한 line.split(",")로 나누면 따옴표 안의 쉼표까지 열 구분으로 오해합니다. 한 칸 안에 줄바꿈이 들어 있는 경우에는 한 줄씩 읽는 방식도 실제 데이터 행과 맞지 않습니다.
05_special_characters.py는 메모 칸에 쉼표와 줄바꿈을 넣어 저장한 뒤 다시 읽습니다. repr()은 문자열 안의 줄바꿈을 실제 개행 대신 \n으로 보여 주므로 한 행이 복원되었는지 확인하기 좋습니다.
import csv
notes = [
["상품", "메모"],
["머그컵", "선물용, 파란색"],
["텀블러", "빨대 포함\n세척 주의"],
]
with open("notes.csv", "w", encoding="utf-8", newline="") as file:
writer = csv.writer(file)
writer.writerows(notes)
with open("notes.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.reader(file)
for row in reader:
print(repr(row))
실행 결과입니다.
['상품', '메모']
['머그컵', '선물용, 파란색']
['텀블러', '빨대 포함\n세척 주의']
저장된 notes.csv는 텍스트 편집기에서 네 줄처럼 보이지만 reader가 돌려준 데이터 행은 세 개입니다. 마지막 메모의 줄바꿈이 따옴표 안에 들어 있어 하나의 필드로 해석되기 때문입니다. 이번 테스트에서는 쉼표와 줄바꿈이 모두 원래 문자열로 복원되었습니다.
이 예제가 newline=""을 읽을 때도 넣는 이유를 보여 줍니다. 파일 기능이 줄바꿈을 먼저 변환하지 않은 상태로 넘겨야 csv 모듈이 따옴표 안의 줄바꿈을 정확히 판단할 수 있습니다.
rb가 아니라 텍스트 모드로 열어야 합니다
CSV는 텍스트 형식입니다. open("orders.csv", "rb")의 b는 문자열이 아닌 바이트를 읽는 바이너리 모드입니다. 반면 csv.reader()는 문자열을 내주는 반복 가능한 값을 입력으로 기대합니다.
06_binary_mode_error.py로 차이를 재현할 수 있습니다.
import csv
with open("orders.csv", "rb") as file:
reader = csv.reader(file)
print(next(reader))
직접 실행한 오류의 마지막 줄입니다.
_csv.Error: iterator should return strings, not bytes (did you open the file in text mode?)
오류 문구 그대로 파일을 텍스트 모드로 열면 됩니다. 읽을 때는 "r", 쓸 때는 "w"를 사용하고, 한글을 포함한다면 읽기와 쓰기에 같은 encoding을 지정하세요.
with open("orders.csv", "r", encoding="utf-8", newline="") as file:
reader = csv.reader(file)
UTF-8로 저장한 파일을 CP949처럼 다른 인코딩으로 읽으면 UnicodeDecodeError가 나거나 글자가 깨질 수 있습니다. 이 경우 csv.reader()의 문제가 아니라 파일을 글자로 바꾸는 인코딩이 맞지 않는 것입니다. 출처를 아는 파일은 만든 쪽의 인코딩을 확인하고, 모르는 파일을 errors="ignore"로 무작정 읽으면 문자가 사라질 수 있으므로 피하는 편이 좋습니다.
오류에 나온 인코딩 이름을 어떻게 해석하고 실제 파일에 맞춰 고칠지는 파이썬 UnicodeDecodeError 해결 : UTF-8과 CP949, Python 3.15 변경점에서 오류 재현과 정상 출력으로 확인할 수 있습니다.
writer와 reader의 흐름만 다시 연결해 봅니다
CSV 쓰기의 흐름은 파이썬 리스트 → writer가 CSV 규칙 적용 → 파일 내용 변경입니다. 읽기의 흐름은 CSV 파일 → reader가 구분 기호와 따옴표 해석 → 문자열 리스트 반환입니다.
처음 연습할 때는 다음 네 가지를 한 묶음으로 기억하면 됩니다.
csv.writer(file)로 writer 객체를 만든다.- 한 행은
writerow(), 여러 행은writerows()로 쓴다. - 파일은 텍스트 모드와
newline=""로 연다. csv.reader()로 읽은 숫자 칸은 계산 전에int()나float()로 바꾼다.
CSV가 단순히 쉼표를 붙인 문자열이었다면 상품명 안의 쉼표와 메모 안의 줄바꿈을 직접 처리해야 했습니다. csv 모듈을 쓰면 그 형식 처리를 맡기고, 코드는 어떤 열을 저장하고 어떤 열을 숫자로 계산할지에 집중할 수 있습니다.
다음 단계에서는 첫 행의 열 이름을 키로 사용하는 DictReader와 DictWriter를 이용해 row[1] 대신 row["수량"]처럼 읽는 방법을 이어서 연습할 수 있습니다.
참고한 공식 문서
'IT프로그래밍 > 파이썬' 카테고리의 다른 글
| 파이썬 UnicodeDecodeError 해결 : UTF-8과 CP949, Python 3.15 변경점 (0) | 2026.09.30 |
|---|---|
| 파이썬 JSON 파일 저장과 읽기 : dump와 load로 딕셔너리 보관하기 (0) | 2026.09.10 |
| 파이썬 FileNotFoundError 해결 : 파일이 있는데도 못 찾는 상대경로 문제 (0) | 2026.09.09 |
| 파이썬 import 사용법 : 직접 만든 모듈에서 함수 불러오기 (0) | 2026.09.07 |
| 파이썬 ModuleNotFoundError 해결 : pip와 실행 Python 경로 맞추기 (0) | 2026.09.04 |
| 파이썬 t-string 사용법 : f-string과 다른 점과 Python 3.14 처리 원리 (0) | 2026.09.02 |
| 파이썬 sorted와 sort 차이 : key와 lambda로 원하는 기준 정렬하기 (0) | 2026.09.01 |
| 파이썬 enumerate와 zip 사용법 : 번호와 두 리스트를 함께 꺼내기 (0) | 2026.08.26 |
댓글