파이썬 공부 Day 2: 변수 · 자료형 · 문자열 · 리스트타입을 안 적는 변수, 바뀌지 않는 문자열, 바뀌는 리스트
C를 먼저 배운 사람에게 파이썬 첫 문법은 묘하게 낯섭니다. 타입을 안 적어도 되고, 나눗셈은 실수를 돌려주고, 문자열은 한 글자도 못 바꿉니다. Day 1 다음 날에는 변수부터 리스트까지 기본 문법을 하루에 훑었습니다.
배운 범위는 네 덩어리입니다.
- 변수: 이름 규칙과 키워드
- 자료형과 연산자:
int,float,bool, 산술·복합 연산자,print() - 문자열: 인덱싱·슬라이싱, 불변, f-string
- 리스트: 생성, 수정, 원본을 바꾸는 메서드
🧱 변수: 타입을 적지 않는 상자
변수는 데이터를 저장하는 메모리 공간에 이름을 붙인 것입니다. 파이썬에서는 값을 대입하는 순간 변수가 생깁니다. 타입은 값을 보고 알아서 정합니다. C처럼 int age = 30;이라고 쓸 필요가 없습니다.
name = "홍길동"
age = 30
print(name, age) # 홍길동 30
print(type(age)) # <class 'int'>
공부하다 본 정의 하나가 기억에 남습니다. 변수에 담은 데이터를 이렇게 저렇게 처리하면 프로그래밍이고, 변수에 담긴 수많은 데이터를 분석하면 데이터 분석입니다.
이름 규칙과 키워드
이름에는 문자, 숫자, 밑줄(_)만 쓰고 숫자로 시작할 수 없습니다. 대소문자도 구분해서 Name과 name은 다른 변수입니다. 숫자로 시작하면 실행 전에 바로 막힙니다.
# 의도적으로 에러를 내는 코드
# SyntaxError: invalid decimal literal
9age = 20
class, if, for 같은 키워드(예약어)도 이름으로 못 씁니다. 제 실습 환경(Python 3.14) 기준으로 35개이고, keyword.kwlist로 목록을 볼 수 있습니다. 이름은 짧은 것보다 뜻이 보이는 쪽이 낫습니다. a = 25보다 user_age = 25가 훨씬 읽기 쉽고, 형식은 PEP 8의 snake_case를 따릅니다.
C와 가장 다른 건 들여쓰기입니다. 중괄호 대신 들여쓰기로 코드 블록을 나누고, 잘못 들여쓰면 바로 에러입니다(스페이스 4칸 권장). 공부하다 본 농담도 하나 적어 뒀습니다. 코드 품질을 재는 유일한 척도는 분당 터져 나오는 "WTF!" 횟수라는 말입니다.
🔢 숫자형과 연산자: 나눗셈은 항상 float
기본 숫자형은 정수 int, 실수 float, 참·거짓 bool입니다. 8진수는 0o, 16진수는 0x로 시작합니다.
print(type(19.99), type(True))
# <class 'float'> <class 'bool'>
print(0o23, 0xB3, 1.0e3) # 19 179 1000.0
a, b = 10, 3
print(a / b) # 3.3333333333333335
print(a // b) # 3
print(a % b) # 1
print(a ** b) # 1000
print(10 / 2) # 5.0
print(-7 // 2) # -4
제가 제일 먼저 걸린 곳이 여기입니다. C에서는 정수끼리 나누면 몫만 남는데, 파이썬의 /는 나눠떨어져도 5.0처럼 float을 돌려줍니다. 정수 몫이 필요하면 //를 써야 합니다. 다만 //는 C처럼 0 쪽으로 버리지 않고 아래로 내려서, -7 // 2가 -3이 아니라 -4입니다.
%와 //는 홀짝 판별과 연령대 계산에 자주 씁니다. a += b처럼 연산과 대입을 한 번에 하는 복합 연산자도 있고, //=나 **=도 같은 방식입니다.
print(23 % 2 == 0) # False
age = 26
print(age // 10 * 10) # 20
age += 1
print(age) # 27
숫자용 내장함수에서는 함정 두 개를 노트에 남겼습니다. ord()는 노트에 아스키 코드라고 적었는데, 실제로는 유니코드 번호라 ord("가")도 44032가 나옵니다. bool은 int의 하위 타입이라 isinstance(True, int)가 True입니다. divmod()와 math 모듈까지 정리한 표는 파이썬 치트시트에 모아 뒀습니다.
print()의 sep과 end
콤마로 값을 여러 개 넘기면 사이에 공백이 자동으로 들어갑니다. sep은 값 사이 구분자, end는 끝에 붙는 문자이고 end의 기본값은 줄바꿈입니다.
print('Hello', 'world', sep=',')
# Hello,world
print('Hello', end='')
print('world')
# Helloworld
🔤 문자열: 읽을 수는 있어도 고칠 수는 없다
따옴표와 연산
문자열은 작은따옴표나 큰따옴표로 감쌉니다. 여러 줄은 삼중따옴표(''', """)로 쓰고, 따옴표 안의 따옴표는 다른 종류로 감싸거나 \"로 이스케이프합니다. 노트에 따로 표시해 둔 함정은 따옴표가 아니라 콤마였습니다. 문자열 두 개를 콤마 없이 나란히 쓰면 하나로 합쳐지고, 콤마를 넣으면 튜플이 됩니다.
quote = "He said \"Hello\""
print(quote) # He said "Hello"
joined = 'Hello' 'Python'
pair = 'Hello', 'Python'
print(joined) # HelloPython
print(pair) # ('Hello', 'Python')
+는 연결, *는 반복입니다. 리포트 머리줄 같은 구분선은 "-" * 30 한 줄이면 됩니다. 숫자와 문자열은 +로 붙일 수 없습니다.
# 의도적으로 에러를 내는 코드
# TypeError: unsupported operand type(s)
# for +: 'int' and 'str'
print(1991 + "년")
타입이 다르면 str(1991) + "년"처럼 먼저 문자열로 바꾼 뒤 연산합니다.
인덱싱과 슬라이싱
인덱스는 0부터 시작하고, 뒤에서 셀 때는 -1부터입니다. 0부터 세는 건 C 배열과 같아서 익숙했습니다. 음수 인덱스는 처음 써 봤습니다.
문자열: P y t h o n
정방향: 0 1 2 3 4 5
역방향: -6 -5 -4 -3 -2 -1
슬라이싱 문법은 [시작:끝:간격]이고, 끝 인덱스는 포함하지 않습니다. [m:n]이면 m부터 n-1까지입니다.
text = "Hello Python World"
print(text[0], text[-1]) # H d
print(text[6:12]) # Python
print(text[13:]) # World
print(text[::2]) # HloPto ol
print(text[::-1]) # dlroW nohtyP olleH
간격에 2를 주면 한 칸씩 건너뛰고, -1을 주면 뒤집힙니다. [::-1]은 뒤집기 관용구로 그냥 외웠습니다.
문자열은 불변이다
C의 char 배열은 s[0] = 'J'로 한 글자를 바꿀 수 있습니다. 파이썬 문자열은 안 됩니다.
# 의도적으로 에러를 내는 코드
# TypeError: 'str' object does not
# support item assignment
text = "Python"
text[0] = "J"
바꾸고 싶으면 새 문자열을 만들어 다시 담습니다. 메서드도 마찬가지입니다. upper()나 replace()는 원본을 그대로 두고 새 문자열을 돌려주고, find()는 정수를, split()은 리스트를 돌려줍니다. 그래서 text.upper()만 쓰면 아무 일도 일어나지 않습니다.
text = "Python"
text = "J" + text[1:]
print(text) # Jython
text.upper()
print(text) # Jython
text = text.upper()
print(text) # JYTHON
print(text[2:100]) # THON
마지막 줄에서 보듯 슬라이싱과 인덱싱은 범위 처리가 다릅니다. 슬라이싱은 범위를 벗어나도 있는 만큼만 잘라 주지만, text[10] 같은 인덱싱은 바로 IndexError입니다.
포맷팅: f-string이 표준
포맷팅 방법은 세 가지입니다.
name, age = "홍길동", 25
print("이름: %s, 나이: %d세" % (name, age))
print("이름: {}, 나이: {}세"
.format(name, age))
print(f"이름: {name}, 나이: {age}세")
# 세 줄 모두: 이름: 홍길동, 나이: 25세
temp, price = 24.567, 1234567
print(f"{age + 1}세") # 26세
print(f"{temp:.1f}°C") # 24.6°C
print(f"{price:,}원") # 1,234,567원
print(f"[{'Hi':>6}]") # [ Hi]
% 포맷팅은 C의 printf와 같은 서식 문자(%s, %d)라 읽기는 쉽습니다. 지금 표준은 f-string이고, 중괄호 안에 age + 1 같은 표현식과 :.1f 같은 서식까지 넣을 수 있습니다. 정렬은 공백이 눈에 안 보여서 대괄호로 감싸 확인했습니다.
자주 쓰는 메서드
capitalize()는 첫 글자만 올리는 게 아니라 나머지를 소문자로 내린다는 점을 노트에 굵게 표시해 뒀습니다. "hELLO".capitalize()는 Hello입니다.
split()과 join()은 처음엔 감이 덜 왔습니다. split()은 문자열을 리스트로, join()은 리스트를 문자열로 만듭니다. 리스트를 배운 뒤 다시 보니 이해가 더 잘 됐습니다.
words = "Life is short".split()
print(words)
# ['Life', 'is', 'short']
print("-".join(words)) # Life-is-short
s = "a b"
print(s.split()) # ['a', 'b']
print(s.split(" ")) # ['a', '', 'b']
# 입력: 10 20
a, b = map(int, input().split())
print(a + b) # 30
split()은 모든 공백을 기준으로 연속 공백을 하나로 묶지만, split(" ")은 스페이스 한 칸마다 잘라서 빈 문자열이 끼어듭니다. 마지막의 map(int, input().split())은 공백으로 구분된 값을 나눠 담는 코딩 테스트 단골 패턴입니다.
공백 제거, 찾기·바꾸기, 자리 맞추기 같은 메서드 전체 표는 파이썬 치트시트에 정리했습니다.
라이브러리 맛보기: datetime
파이썬은 import로 미리 만들어진 라이브러리를 가져다 씁니다. datetime.now()는 실행할 때마다 값이 달라서, 날짜를 고정해 결과를 확인했습니다.
from datetime import datetime
d = datetime(2026, 10, 23, 14, 30)
print(d.strftime("%Y-%m-%d %H:%M"))
# 2026-10-23 14:30
print(d.weekday()) # 4
weekday()는 월요일이 0, 일요일이 6이라 금요일인 이날은 4가 나옵니다. 이 함수는 Day 4 요일 문제에서 다시 씁니다.
📋 리스트: 문자열과 닮았지만 바꿀 수 있다
리스트는 여러 값을 순서대로 담는 컨테이너 자료형입니다. 장바구니 목록이나 학생 점수처럼 값이 여럿일 때 이름 하나로 묶어 다룹니다. 특징은 이렇게 정리했습니다.
- 대괄호
[]안에 콤마로 요소를 나열합니다. - 순서가 있어서 문자열처럼 인덱싱과 슬라이싱이 됩니다.
- 같은 값이 중복돼도 됩니다.
- 정수, 문자열, 딕셔너리 등 타입을 섞어 담을 수 있습니다. 한 타입만 담는 C 배열과 다른 점입니다.
mixed = [1, "hello", 3.14, True]
score = [[80, 70, 90],
[85, 70, 90],
[90, 75, 80]]
print(len(score)) # 3
print(score[1][2]) # 90
print(list(range(0, 10, 2)))
# [0, 2, 4, 6, 8]
len(score)가 9가 아니라 3인 이유는 바깥 리스트의 요소가 리스트 3개이기 때문입니다. score[1][2]는 1번 줄의 2번 칸이고, 연속된 정수 리스트는 range()로 만듭니다.
슬라이싱과 a[-n:] 함정
규칙은 문자열과 똑같습니다. 간격이 음수면 시작이 끝보다 오른쪽에 있어야 하고, 이때도 끝 위치는 빠집니다.
a = [0, 1, 2, 3, 4, 5]
print(a[-3:]) # [3, 4, 5]
print(a[:-1]) # [0, 1, 2, 3, 4]
print(a[4:1:-1]) # [4, 3, 2]
print(a[1:4:-1]) # []
n = 0
print(a[-n:]) # [0, 1, 2, 3, 4, 5]
print(a[len(a) - n:]) # []
a[-n:]으로 뒤에서 n개를 자를 때는 n이 0인 경우를 조심해야 합니다. -0은 그냥 0이라 빈 리스트가 아니라 전체가 나옵니다. n이 0일 수 있으면 a[len(a) - n:]로 씁니다. 다만 이 식은 n이 길이보다 크면 시작이 음수가 돼서 다시 틀립니다.
얕은 복사와 [[0] * m] * n
a[:]는 전체 복사인데 얕은 복사입니다. 바깥 리스트만 새로 만들고, 안쪽 리스트는 원본과 공유합니다. 2차원 리스트를 [[0] * m] * n으로 만들 때 제일 많이 틀리는 것도 같은 원리입니다. 안쪽 리스트 하나를 n번 가리키기만 해서, 한 칸을 바꾸면 모든 행이 같이 바뀝니다.
import copy
grid = [[1, 2], [3, 4]]
shallow = grid[:]
deep = copy.deepcopy(grid)
shallow[0][0] = 99
print(grid) # [[99, 2], [3, 4]]
print(deep) # [[1, 2], [3, 4]]
bad = [[0] * 3] * 2
good = [[0] * 3 for _ in range(2)]
bad[0][0] = 1
good[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0]]
print(good) # [[1, 0, 0], [0, 0, 0]]
안쪽까지 통째로 떼어 내려면 copy.deepcopy()를 씁니다. 행마다 새 리스트를 만들려면 컴프리헨션을 씁니다(문법은 Day 4에서 다룹니다).
수정·추가·삭제: 여기서 문자열과 갈린다
리스트는 만든 뒤에도 값을 바꿀 수 있습니다(가변, mutable). 문자열과 가장 크게 다른 점이 이것입니다. 더하면 결합, 숫자를 곱하면 반복인 건 문자열과 같습니다.
nums = [10, 20, 30, 40]
nums[1] = 25
nums += [50]
nums.extend([60, 70])
print(nums)
# [10, 25, 30, 40, 50, 60, 70]
nums[10:] = [100]
print(nums[-2:]) # [70, 100]
del nums[0]
del nums[2:4]
print(nums) # [25, 30, 60, 70, 100]
nums[10:] = [100]은 의외였습니다. 범위 밖 슬라이스에 대입했는데 에러 없이 맨 뒤에 붙습니다. 슬라이싱이 범위에 관대하다는 문자열 쪽 규칙이 여기서도 이어집니다.
슬라이스 대입은 길이가 달라도 됩니다. 간격을 준 슬라이스는 대입할 때 길이가 같아야 하지만(다르면 ValueError), del로 지우는 건 됩니다. a[:] = []는 clear()처럼 같은 객체를 비웁니다.
a = [0, 1, 2, 3, 4, 5]
a[1:3] = [9, 9, 9]
print(a) # [0, 9, 9, 9, 3, 4, 5]
del a[::2]
print(a) # [9, 9, 4]
b = a
a[:] = []
print(a, b) # [] []
a = []였다면 a만 새 빈 리스트를 가리키고, b는 [9, 9, 4]를 그대로 들고 있었을 겁니다.
append()와 extend()는 비슷해 보여도 결과가 다릅니다.
a, b = [1, 2], [1, 2]
a.append([3, 4])
b.extend([3, 4])
print(a) # [1, 2, [3, 4]]
print(b) # [1, 2, 3, 4]
append()는 리스트를 통째로 요소 1개로 넣고, extend()는 풀어서 요소 2개로 붙입니다. 리스트 메서드 중 가장 많이 쓰는 건 append()라서, 다른 건 몰라도 이거 하나만은 확실히 익혀 두기로 했습니다.
제일 많이 하는 실수: a = a.sort()
sort(), reverse(), append()처럼 원본을 바꾸는 메서드는 None을 반환합니다. 결과를 다시 담으면 리스트가 사라집니다.
a = [3, 1, 2]
a = a.sort()
print(a) # None
b = [3, 1, 2]
c = sorted(b)
print(b, c) # [3, 1, 2] [1, 2, 3]
정렬된 새 리스트가 필요하면 sorted()를 씁니다. 문자열 메서드는 결과를 다시 담아야 하고, 리스트의 원본 변경 메서드는 다시 담으면 안 됩니다. 정반대라서 노트에 "원본을 바꿈"과 "새 값을 반환"을 기호로 나눠 표시해 뒀습니다(예외는 pop() 하나로, 원본을 바꾸면서 꺼낸 값을 돌려줍니다).
괄호 모양이 자료형을 정한다
여러 값을 묶는 대표 컨테이너 자료형 4개는 괄호 모양으로 갈립니다.
| 예시 | 자료형 | 배우는 날 |
|---|---|---|
[1, 2, 3] | 리스트 | Day 2 |
(1, 2, 3) | 튜플 | Day 3 |
{1, 2, 3} | 집합 | Day 3 |
{"a": 1} | 딕셔너리 | Day 3 |
괄호 종류가 셋뿐이라 중괄호는 집합과 딕셔너리가 같이 씁니다. 안에 키: 값 형태가 들어가면 딕셔너리가 됩니다.
규칙에 예외도 둘 있습니다. 튜플은 사실 괄호보다 콤마가 만들고(앞에서 본 'Hello', 'Python'), 빈 {}는 집합이 아니라 빈 딕셔너리입니다. 나머지 셋은 Day 3에서 이어집니다.
치트시트: 외우려고 따로 뽑은 것
외우기용 목록과 메서드·함수 표는 분량이 커서 파이썬 치트시트로 옮겼습니다. 여기에는 정리하다 걸린 두 가지만 남깁니다.
round()는 노트에 반올림이라고 적었지만, 딱 .5일 때는 짝수 쪽으로 붙습니다. 그래서 round(2.5)가 3이 아니라 2입니다. input()은 항상 문자열을 돌려주니 숫자로 쓰려면 int(input())처럼 바꿔야 합니다.
print(round(2.5), round(3.5)) # 2 4
print(round(3.14159, 2)) # 3.14
print(int("10") + 1) # 11
입력이 숫자인지 미리 검사하는 isdigit(), isnumeric(), isdecimal()의 차이는 Day 4에서 다룹니다.
입력은 map(int, input().split()), 문자열은 split·join·strip·replace, 리스트는 append·pop·sort, 집계는 len·sum·max·min. 이 조합만 손에 익어도 Day 2 범위 문제는 거의 다 풀립니다.
둘째 날 한 줄 정리
문자열과 리스트는 둘 다 순서가 있어서 인덱싱·슬라이싱이 똑같이 됩니다. 차이는 하나입니다. 문자열은 불변이라 메서드가 새 값을 돌려주고, 리스트는 가변이라
append와sort가 원본을 바꿉니다.
노트에 경고 표시를 가장 많이 붙인 곳도 결국 이 경계였습니다. 파이썬 셸을 열고 a = [3, 1, 2] 다음에 print(a.sort())를 쳐 보면, 왜 None이 나오는지 한 번에 감이 옵니다.