문자열과 리스트: 불변 객체와 가변 객체
Python의 str은 Unicode 코드 포인트로 이루어진 불변(immutable) 시퀀스다. 순서가 있으므로 인덱싱(text[0])과 슬라이싱(text[1:3])을 사용할 수 있다. 하지만 문자열을 만든 뒤에는 특정 위치의 문자를 직접 수정할 수 없다.
문자열을 변경하는 것처럼 보이는 대입, 대소문자 변환, 치환 등의 연산도 원본을 수정하는 것이 아니라 새로운 문자열을 만들어 반환한다. 아래 예제처럼 닉네임을 변경하면 기존 문자열이 수정되는 것이 아니라 새로운 문자열이 생성되어 변수 nickname이 그 문자열을 가리키게 된다.
# 문자열(str)은 불변(immutable)이다. 닉네임을 변경하면 기존 문자열을 수정하는 것이 아니라 새로운 문자열을 만들어 변수(nickname)가 그것을 가리킨다.
nickname = "wizard99"
print(nickname)
print(id(nickname)) # 기존 닉네임이 저장된 위치
# 닉네임 변경
nickname = "wizard88"
print(nickname)
print(id(nickname)) # 새로운 문자열이 저장된 위치 (기존과 다름)
print("-" * 40)
# 리스트(list)는 가변(mutable)이다. 리스트 안의 값을 바꿔도 같은 객체를 계속 사용한다.
nicknames = ["wizard99"]
print(nicknames)
print(id(nicknames)) # 리스트가 저장된 위치
# 리스트의 첫 번째 요소(닉네임) 수정
nicknames[0] = "wizard88"
print(nicknames)
print(id(nicknames)) # 같은 리스트를 수정했으므로 위치가 그대로
wizard99
4324163664
wizard88
4324196432
----------------------------------------
['wizard99']
4324201984
['wizard88']
4324201984
실행 결과에서 문자열은 id()가 바뀌지만 리스트는 그대로인 것을 확인할 수 있다. 즉 문자열은 변경 시 새로운 객체가 생성되고, 리스트는 기존 객체의 내용을 수정한다.
id()값은 객체가 저장된 위치를 식별하는 값이며, 실행할 때마다 달라질 수 있다.
str과 bytes는 역할도 다르다. str은 사람이 읽는 텍스트를 표현하고, bytes는 파일이나 네트워크에서 사용하는 바이트 데이터를 표현한다. bytes를 텍스트로 사용할 때는 decode()로 str로 변환하고, str을 바이트로 저장하거나 전송할 때는 encode()를 사용한다. 데이터 분석에서는 컬럼 이름이나 범주형 값은 대부분 str로 다루며, 파일을 읽을 때는 문자 인코딩을 명시적으로 확인하는 것이 좋다.
인덱싱과 슬라이싱의 경계는 다르다
인덱싱 text[i]는 위치 하나를 골라 길이 1인 str을 반환한다. 범위를 벗어나면 IndexError가 난다. 슬라이싱 text[start:end:step]은 start부터 end 직전까지 새 문자열을 만들며, 범위를 벗어난 경계는 가능한 범위로 조정된다.
start 생략: 시퀀스의 시작점부터 선택한다.
end 생략: 시퀀스의 끝까지 선택한다. end 자체는 포함하지 않는다.
step 생략: 기본값 1이다. 2이면 한 칸씩 건너뛰고, 음수이면 뒤쪽 방향으로 진행한다.
step이 0이면 진행 방향이 없으므로 ValueError가 발생한다.
text = "DATA-2026"
print("1.", text[0]) # 첫 번째 문자
print("2.", text[0:4]) # 0~3번째 문자
print("3.", text[:4]) # 처음부터 3번째 문자까지
print("4.", text[5:]) # 5번째 문자부터 끝까지
print("5.", text[::2]) # 한 칸씩 건너뛰기
print("6.", text[::-1]) # 문자열 뒤집기
print("7.", repr(text[100:]))# 범위를 벗어난 슬라이싱
#print("8.", text[::0])# ValueError 에러나옴
1. D
2. DATA
3. DATA
4. 2026
5. DT-06
6. 6202-ATAD
7. ''
음수 인덱스는 뒤에서 센다. text[-1]은 마지막 문자 6, text[-4:]은 2026이다. 다만 인덱스 -1도 빈 문자열에서는 범위를 벗어나므로, 입력이 비어 있을 수 있다면 먼저 길이나 truth value를 확인한다.
strip·split·join으로 텍스트를 정제한다
strip은 양끝의 공백을 제거하고, split은 구분자를 기준으로 문자열을 list로 나누며, join은 문자열 iterable을 하나로 연결한다. 셋 모두 원본 str을 변경하지 않는다.
raw = " apple, banana ,pear "
parts = [part.strip() for part in raw.strip().split(",")]
print(parts)
print(" | ".join(parts))
['apple', 'banana', 'pear']
apple | banana | pear
구분자가 연속되면 빈 항목이 생길 수 있고, 인자를 생략한 split은 연속 공백을 하나의 구분 영역처럼 처리한다. CSV처럼 따옴표·이스케이프 규칙이 있는 형식은 직접 split하지 말고 표준 csv 모듈이나 해당 파서를 사용한다.
세 가지 문자열 포매팅 방법
Python에는 오래된 % 연산자, str.format 메서드, f-string이 모두 있다. 같은 값으로 결과를 비교해 보자.
name = "Ada"
score = 91.256
print("%s: %.2f" % (name, score))
print("{}: {:.2f}".format(name, score))
print(f"{name}: {score:.2f}")
Ada: 91.26
Ada: 91.26
Ada: 91.26
% 포매팅: 기존 코드와 logging 호출에서 자주 만난다. 형식 문자와 값의 타입·개수를 맞춰야 한다.
str.format: 위치·이름 필드와 재사용 가능한 템플릿에 적합하다. 템플릿을 데이터로 다루는 설계에서도 선택할 수 있다.
f-string: 코드 안의 지역 변수와 식을 직접 보간해 읽기 쉽다. 하지만 모든 템플릿·번역·로깅 상황에서 항상 최선인 것은 아니다.
f-string 형식 지정 미니 언어
Python에는 오래된 % 연산자, str.format() 메서드, f-string이 모두 있다. 같은 값으로 결과를 비교해 보자.
amount = 1234567.5
label = "revenue"
print(f"|{'left':<8}|")
print(f"|{'right':>8}|")
print(f"|{'mid':^8}|")
print(f"{amount:,.2f}")
print(f"{label=}")
print(f"{label!r}")
|left |
| right|
| mid |
1,234,567.50
label='revenue'
'revenue'
<, >, ^: 지정한 너비 안에서 왼쪽·오른쪽·가운데 정렬한다.
12,.2f: 전체 최소 너비 12, 천 단위 쉼표, 소수점 이하 2자리인 고정소수점 표현이다.
변수명 뒤 =: 디버깅할 때 표현식과 값을 함께 보여 준다. !r은 repr 변환을 적용해 따옴표와 이스케이프를 확인하기 좋다.
외부 입력은 포매팅만으로 안전해지지 않는다
f-string은 출력 문자열을 만드는 도구이지 보안 검증이나 이스케이프 도구가 아니다. 사용자가 입력한 값을 SQL 문장에 직접 보간하지 말고 데이터베이스 드라이버의 매개변수 바인딩을 사용한다. 셸 명령도 문자열 하나로 합치지 말고 인자 배열로 전달하며 shell 실행을 피한다.
HTML·URL·CSV·로그처럼 출력 대상마다 필요한 인코딩과 이스케이프 규칙이 다르다. 먼저 출력 경계를 정하고 해당 라이브러리의 안전한 API를 선택한다. 값의 보기 좋은 형식과 신뢰 경계의 안전성은 별개의 문제다.
실습: 고정 너비 보고서 행 만들기
rows = [
("Ada", 1234.5, True),
("Linus", 90, False),
]
for name, amount, ok in rows:
status = "OK" if ok else "CHECK"
print(f"{name:<6} | {amount:>8,.1f} | {status}")
Ada | 1,234.5 | OK
Linus | 90.0 | CHECK
이 행은 이름을 6칸 왼쪽 정렬하고 금액을 8칸 오른쪽 정렬하며 소수점 한 자리와 천 단위 쉼표를 적용한다. 사람이 읽는 텍스트 보고서에는 유용하지만, 다른 프로그램이 읽어야 할 데이터는 정렬 공백에 의존하지 말고 CSV·JSON 같은 구조화 형식으로 저장한다.
흔한 실수와 핵심 체크리스트
☐ 슬라이스의 end가 포함되지 않는다는 점을 예제로 확인한다.
☐ 범위를 벗어난 단일 인덱스는 IndexError, 슬라이스 경계는 조정된다는 차이를 안다.
☐ str은 불변이므로 정제 메서드의 반환값을 새 변수에 저장하거나 다시 대입한다.
☐ [::-1]이 모든 사용자 표시 문자를 안전하게 뒤집는다고 가정하지 않는다.
☐ 포맷 지정에서 너비·정렬·정밀도·천 단위 구분의 역할을 구분한다.
☐ 외부 입력을 SQL·셸에 직접 보간하지 않고 대상 시스템의 안전한 매개변수 API를 사용한다.
다음 글: 제너레이터와 yield
문자열 슬라이스가 새 객체를 만드는 비용을 살펴봤다. 다음 글에서는 모든 값을 한꺼번에 메모리에 만들지 않고 필요할 때 하나씩 생산하는 generator expression과 yield, 반복 소비 시 주의점을 다룬다.
참고 자료
Python 공식 튜토리얼: Strings — 인덱싱, 슬라이싱 경계와 문자열 불변성 (2026-08-03 확인)
Python 표준 타입 문서: Text Sequence Type — str — Unicode 시퀀스, strip·split·join과 문자열 연산 (2026-08-03 확인)
Python 언어 레퍼런스: f-strings — 표현식 평가, 변환과 format specifier 문법 (2026-08-03 확인)
Python string 문서: Format Specification Mini-Language — 너비, 정렬, 정밀도와 숫자 그룹핑 규칙 (2026-08-03 확인)
Python Unicode HOWTO — 코드 포인트, str와 bytes, 인코딩·디코딩의 구분 (2026-08-03 확인)
댓글 0
댓글을 불러오는 중…