PHpullh
심층 가이드/Python/기초 문법

PYTHON · 심층 가이드

Python 기초 문법 완전 정리

타입 힌트가 런타임에 무엇을 하고 하지 않는지부터 f-string 포매팅, 컴프리헨션, match/case, Counter와 defaultdict까지 Python 문법의 실제 동작을 정리합니다.

주제 29개 · 예제 코드 포함 · 최종 수정 2026-08-30 · 작성 pullh 편집팀

Python의 문법은 얕아 보이지만 밑에 깔린 규칙은 다른 언어와 꽤 다릅니다. 변수는 값을 담는 상자가 아니라 객체에 붙는 이름표이고, 타입 힌트는 검사기를 위한 메타데이터일 뿐 인터프리터가 강제하지 않습니다. x: int = "문자열"은 그대로 실행됩니다. 이 사실을 모르면 힌트를 믿고 방어 코드를 빼거나, 반대로 힌트를 아예 장식으로 취급하게 됩니다. 힌트는 mypy나 pyright가 읽을 때 비로소 값을 하고, 실행 시간의 안전은 여전히 코드가 책임집니다.

이 가이드의 항목들은 세 갈래로 묶입니다. 변수 & 타입 힌트에서 시작한 흐름이 타입 별칭, Literal 타입, 유니온 타입, 그리고 3.12의 PEP 695 타입 파라미터로 이어지며 표현력을 넓힙니다. 데이터를 다루는 축에서는 슬라이싱 심화리스트 컴프리헨션 & 제너레이터가 짝을 이루고, 여기에 딕셔너리 심화, Counter 카운터, defaultdict가 붙어 집계 코드를 짧게 만듭니다. 제어 흐름 축은 조건문 & 반복문에서 walrus 연산자match/case 패턴 매칭으로 확장됩니다.

match에서 가장 많이 밟는 지뢰가 있습니다. case RED:처럼 점이 없는 단순 이름은 상수와 비교하는 게 아니라 무조건 매칭되고 그 이름에 값을 바인딩하는 캡처 패턴입니다. 아래 case들이 전부 죽은 코드가 됩니다. 상수를 비교하려면 case Color.RED:처럼 점 붙은 참조를 쓰거나 리터럴을 직접 적어야 합니다. defaultdict도 비슷하게 조용합니다. 없는 키를 읽기만 해도 기본값이 생성되어 저장되므로, 조회 목적이면 get()이나 in을 쓰는 편이 낫습니다.

01변수 & 타입 힌트

Python의 동적 타이핑과 Python 3.5+의 타입 힌트(Type Hints)로 코드 안정성을 높입니다.

Python code

# 동적 타이핑 — 타입 선언 불필요
name = "Python"
version = 3.12
is_awesome = True
count = 100

# 타입 힌트 (Python 3.5+) — 런타임 강제 아님, IDE/mypy용
salary: int = 50_000_000
user_name: str = "Alice"
ratio: float = 0.75
active: bool = True

# 여러 변수 동시 할당
x, y, z = 1, 2, 3
a = b = c = 0

# 타입 확인
print(type(name))     # <class 'str'>
print(type(version))  # <class 'float'>
print(isinstance(name, str))  # True

# 언더스코어로 숫자 가독성
population = 51_000_000
pi = 3.141_592_653

# None — null에 해당
result = None
print(result is None)  # True (== 대신 is 사용)
알아두면 좋은 점

None 비교는 ==가 아닌 is/is not을 사용하세요. 일부 클래스에서 ==가 예상치 못한 결과를 줄 수 있습니다.

자주 하는 실수

True/False는 대문자로 시작합니다. true/falseNameError입니다. Java/Kotlin 개발자가 자주 하는 실수입니다.

02문자열 — f-string &amp; 메서드

Python의 강력한 문자열 처리. f-string, 메서드 체이닝, 슬라이싱을 완전히 익힙니다.

Python code

name = "  python developer  "
score = 98.765

# f-string 포맷팅 (Python 3.6+)
print(f"이름: {name.strip()}")
print(f"점수: {score:.2f}")          # 소수점 2자리
print(f"퍼센트: {score / 100:.1%}") # 백분율
print(f"{'중앙정렬':^20}")           # 패딩/정렬
print(f"{1000000:,}")               # 천단위 콤마

# 주요 문자열 메서드
s = "Hello, Python World"
print(s.upper())           # HELLO, PYTHON WORLD
print(s.lower())           # hello, python world
print(s.replace("Python", "Kotlin"))
print(s.split(", "))       # ['Hello', 'Python World']
print(s.startswith("Hello"))  # True
print(s.strip())           # 앞뒤 공백 제거
print(s.find("Python"))    # 7 (인덱스)
print(s.count("l"))        # 3

# 슬라이싱 [start:stop:step]
text = "Hello World"
print(text[0:5])    # Hello
print(text[-5:])    # World
print(text[::-1])   # dlroW olleH (역순)

# join
words = ["Python", "is", "awesome"]
print(" ".join(words))  # Python is awesome
알아두면 좋은 점

f-string에서 ! 변환 플래그를 사용할 수 있습니다: !r(repr), !s(str), !a(ascii).

자주 하는 실수

str.find()는 없으면 -1을 반환합니다. str.index()는 없으면 ValueError를 발생시킵니다. 용도에 맞게 선택하세요.

03조건문 &amp; 반복문

Python의 if/elif/else, for, while 문법과 else 절.

Python code

score = 85

# if / elif / else
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

# 한 줄 조건식 (삼항 연산자)
result = "합격" if score >= 60 else "불합격"

# 연속 비교 (Python만의 특징)
if 80 <= score < 90:
    print("B등급")

# for + range
for i in range(5):          # 0 1 2 3 4
    print(i, end=" ")

for i in range(1, 10, 2):   # 1 3 5 7 9
    print(i, end=" ")

# enumerate — 인덱스와 값 동시에
fruits = ["🍎", "🍌", "🍇"]
for idx, fruit in enumerate(fruits, start=1):
    print(f"{idx}: {fruit}")

# zip — 두 리스트 병렬 순회
names  = ["Alice", "Bob"]
scores = [95, 87]
for name, sc in zip(names, scores):
    print(f"{name}: {sc}")

# for-else: break 없이 완료되면 else 실행
for n in range(2, 10):
    if 7 % n == 0 and n != 7:
        print(f"{n}는 7의 약수")
        break
else:
    print("7은 소수")
알아두면 좋은 점

for-elseelse는 루프가 break 없이 정상 완료됐을 때 실행됩니다. 검색 알고리즘에서 "찾지 못했을 때" 처리에 유용합니다.

자주 하는 실수

Python에는 switch/case가 없었지만 3.10+에서 match/case가 추가됐습니다. 하위 버전 호환이 필요하면 딕셔너리 기반 디스패치를 사용하세요.

04리스트 컴프리헨션 &amp; 제너레이터

Python의 가장 Pythonic한 기법. 간결하고 빠른 컬렉션 생성.

Python code

# 리스트 컴프리헨션
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 조건 필터
evens = [x for x in range(20) if x % 2 == 0]

# 중첩 컴프리헨션
matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)]
flat   = [x for row in matrix for x in row]

# 딕셔너리 컴프리헨션
words = ["python", "kotlin", "golang"]
word_len = {w: len(w) for w in words}
# {'python': 6, 'kotlin': 6, 'golang': 6}

# 세트 컴프리헨션 (중복 제거)
letters = {c for c in "programming" if c not in "aeiou"}

# 제너레이터 표현식 (메모리 효율)
gen = (x**2 for x in range(1_000_000))   # 즉시 계산 안 함
print(next(gen))   # 0
print(next(gen))   # 1

# sum()에 제너레이터 전달 (괄호 하나로)
total = sum(x**2 for x in range(100))

# walrus operator := (Python 3.8+)
data = [1, -2, 3, -4, 5]
positive = [y for x in data if (y := x * 2) > 0]
print(positive)   # [2, 6, 10]
알아두면 좋은 점

제너레이터 표현식은 (), 리스트 컴프리헨션은 []입니다. 큰 데이터를 한 번만 순회하면 제너레이터가 메모리 효율적입니다.

자주 하는 실수

중첩이 3단계 이상 되면 가독성이 급격히 떨어집니다. 이 경우 일반 for 루프나 함수로 분리하세요.

05match/case 패턴 매칭 (Python 3.10+)

구조적 패턴 매칭. 단순 switch 이상의 강력한 패턴 분해를 제공합니다.

Python code

# 기본 match/case
def http_status(status):
    match status:
        case 200: return "OK"
        case 404: return "Not Found"
        case 500 | 503: return "Server Error"   # OR 패턴
        case _:   return "Unknown"

# 구조 분해 패턴
def process_command(command):
    match command.split():
        case ["quit"]:
            return "종료"
        case ["go", direction]:
            return f"{direction}(으)로 이동"
        case ["go", direction, speed]:
            return f"{direction}으로 {speed} 속도로 이동"
        case _:
            return "알 수 없는 명령"

# 클래스 패턴
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

def where_is(point):
    match point:
        case Point(x=0, y=0): return "원점"
        case Point(x=0, y=y): return f"Y축 위 {y}"
        case Point(x=x, y=0): return f"X축 위 {x}"
        case Point(x=x, y=y): return f"({x}, {y})"

print(where_is(Point(0, 5)))    # Y축 위 5
print(process_command("go north fast"))
알아두면 좋은 점

case에서 변수에 바인딩하려면 이름 앞에 아무것도 붙이지 않으면 됩니다. 상수와 비교하려면 Enum이나 클래스.속성 형태를 사용하세요.

자주 하는 실수

case status_code처럼 단독 이름을 쓰면 상수 비교가 아니라 변수 바인딩이 됩니다. 상수와 비교하려면 case MyEnum.VALUE 형태를 사용하세요.

06Walrus 연산자 &amp; 구조적 패턴

Python 3.8+ 대입 표현식(:=)으로 중복 계산을 제거합니다.

Python code

import re

# walrus := — 할당과 평가를 동시에
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# while 루프에서 유용
numbers = iter(data)
while (n := next(numbers, None)) is not None:
    if n % 2 == 0:
        print(f"짝수: {n}")

# 정규식 매치 결과 재사용
text = "Python 3.12 출시"
if m := re.search(r"Python (\d+\.\d+)", text):
    print(f"버전: {m.group(1)}")   # 3.12

# 컴프리헨션에서 중복 계산 제거
def heavy(x): return x * x

# 나쁜 예: heavy() 두 번 호출
results_bad = [heavy(x) for x in range(10) if heavy(x) > 25]

# 좋은 예: walrus로 한 번만 호출
results = [y for x in range(10) if (y := heavy(x)) > 25]
print(results)  # [36, 49, 64, 81]
알아두면 좋은 점

:=는 가독성이 중요한 코드에서는 주의해서 사용하세요. 남용하면 코드를 읽기 어렵게 만들 수 있습니다.

자주 하는 실수

Walrus 연산자로 할당된 변수는 컴프리헨션 바깥 스코프에도 영향을 줍니다. 의도치 않은 변수 누출에 주의하세요.

07Descriptor &amp; __get__ __set__

Python의 속성 접근 메커니즘을 제어하는 디스크립터 프로토콜.

Python code

class Validator:
    """값 검증 디스크립터"""
    def __set_name__(self, owner, name):
        self.name = name
        self.private = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self  # 클래스에서 접근 시
        return getattr(obj, self.private, None)

    def __set__(self, obj, value):
        self._validate(value)
        setattr(obj, self.private, value)

    def _validate(self, value): ...

class PositiveInt(Validator):
    def _validate(self, value):
        if not isinstance(value, int) or value <= 0:
            raise ValueError(f"{self.name}은 양의 정수여야 함: {value}")

class RangeFloat(Validator):
    def __init__(self, lo, hi):
        self.lo, self.hi = lo, hi

    def _validate(self, value):
        if not (self.lo <= value <= self.hi):
            raise ValueError(f"{self.name}은 {self.lo}~{self.hi} 범위: {value}")

class Product:
    price    = PositiveInt()
    quantity = PositiveInt()
    discount = RangeFloat(0.0, 1.0)

    def __init__(self, price, quantity, discount):
        self.price = price
        self.quantity = quantity
        self.discount = discount

    @property
    def total(self):
        return self.price * self.quantity * (1 - self.discount)

p = Product(1000, 5, 0.1)
print(p.total)   # 4500.0

try:
    p.price = -100
except ValueError as e:
    print(e)
알아두면 좋은 점

__set_name__은 Python 3.6+에서 클래스 정의 시 자동으로 호출됩니다. 디스크립터가 어느 클래스의 어떤 이름으로 사용되는지 알 수 있습니다.

자주 하는 실수

__get__에서 obj is None 체크를 하지 않으면 클래스에서 직접 접근할 때(Product.price) 오류가 발생합니다.

08PEP 695 타입 파라미터 (3.12+)

type X = ... 문법으로 타입 별칭과 제네릭 정의

Python code

<span class="cm">// PEP 695 타입 파라미터 (3.12+) 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("PEP 695 타입 파라미터 (3.12+)") }
알아두면 좋은 점

PYTHON 공식 문서를 함께 참고하세요.

자주 하는 실수

자주 발생하는 실수에 주의하세요.

09match-case 패턴 매칭 (Python 3.10+)

구조적 패턴 매칭은 값, 타입, 구조를 한 번에 분해하고 매칭합니다. 단순 switch-case를 넘어 중첩 객체, 시퀀스, 매핑 등 복잡한 데이터 구조를 우아하게 처리할 수 있습니다.

Python code

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

def describe_shape(shape):
    match shape:
        # 리터럴 매칭
        case 0:
            return "영(zero)"

        # 클래스 패턴 + 가드
        case Point(x=0, y=0):
            return "원점"
        case Point(x, y) if x == y:
            return f"대각선 위의 점 ({x}, {y})"
        case Point(x, y):
            return f"점 ({x}, {y})"

        # 중첩 패턴
        case Circle(center=Point(0, 0), radius=r):
            return f"원점 중심 원, 반지름={r}"
        case Circle(center=c, radius=r) if r > 10:
            return f"큰 원: 중심={c}, 반지름={r}"

        # 시퀀스 패턴
        case [x]:
            return f"단일 원소: {x}"
        case [x, y, *rest]:
            return f"첫 둘: {x}, {y} (나머지 {len(rest)}개)"

        # 매핑 패턴
        case {"type": "user", "name": name, **extra}:
            return f"사용자: {name}, 추가정보: {extra}"

        # 와일드카드
        case _:
            return "알 수 없는 형태"

# 테스트
shapes = [
    Point(0, 0), Point(3, 3), Point(1, 2),
    Circle(Point(0, 0), 5), Circle(Point(1, 1), 15),
    [42], [1, 2, 3, 4, 5],
    {"type": "user", "name": "홍길동", "age": 30},
    "기타",
]

for s in shapes:
    print(f"{str(s):>45} → {describe_shape(s)}")
알아두면 좋은 점

case 절의 변수는 바인딩(캡처)됩니다. 상수와 비교하려면 점 표기법(Status.ACTIVE)을 사용하거나 가드(if)를 추가하세요.

자주 하는 실수

case x:는 "변수 x에 바인딩"을 의미하지 "x와 비교"가 아닙니다. case 변수명:은 항상 매칭되므로 상수 비교에는 리터럴이나 case _ if val == x:를 사용하세요.

10슬라이싱 심화

Python 슬라이싱의 고급 기법으로 step 파라미터, 음수 인덱스, 다차원 슬라이싱까지 다룹니다.

Python code

data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# step을 이용한 슬라이싱
print(data[::2])      # [0, 2, 4, 6, 8] 짝수 인덱스
print(data[1::2])     # [1, 3, 5, 7, 9] 홀수 인덱스
print(data[::-1])     # 역순

# slice 객체로 재사용
every_third = slice(None, None, 3)
print(data[every_third])  # [0, 3, 6, 9]

# 슬라이스 대입
data[2:5] = [20, 30, 40]
print(data)  # [0, 1, 20, 30, 40, 5, 6, 7, 8, 9]

# 길이가 다른 대입 (확장/축소)
data[2:5] = [99]
print(data)  # [0, 1, 99, 5, 6, 7, 8, 9]
알아두면 좋은 점

slice 객체를 변수에 저장하면 여러 시퀀스에 동일한 슬라이싱을 재사용할 수 있습니다.

자주 하는 실수

data[::-1]은 새 리스트를 생성합니다. 원본을 뒤집으려면 data.reverse()를 사용하세요.

11문자열 메서드

자주 쓰이는 문자열 메서드를 정리합니다. str은 불변(immutable)이므로 항상 새 문자열을 반환합니다.

Python code

text = "  Hello, Python World!  "

# 공백 제거
print(text.strip())       # "Hello, Python World!"
print(text.lstrip())      # "Hello, Python World!  "

# 검색
print(text.find("Python"))     # 9 (인덱스)
print(text.count("l"))         # 3
print("Python" in text)        # True

# 변환
print(text.strip().upper())    # "HELLO, PYTHON WORLD!"
print(text.strip().title())    # "Hello, Python World!"
print(text.strip().replace("World", "세계"))

# 분할과 결합
words = "a,b,c,d".split(",")
print(words)               # ['a', 'b', 'c', 'd']
print("-".join(words))     # "a-b-c-d"

# 판별 메서드
print("12345".isdigit())   # True
print("hello".isalpha())   # True
알아두면 좋은 점

str.removeprefix()str.removesuffix()는 Python 3.9+에서 사용 가능한 편리한 메서드입니다.

자주 하는 실수

str.replace()는 원본을 변경하지 않고 새 문자열을 반환합니다. 결과를 변수에 재할당해야 합니다.

12숫자 포맷

f-string과 format()을 사용한 숫자 포맷팅 기법입니다. 천 단위 구분, 소수점 자릿수, 퍼센트 등을 다룹니다.

Python code

pi = 3.141592653589793
big = 1234567890

# 소수점 자릿수
print(f"{pi:.2f}")          # 3.14
print(f"{pi:.4f}")          # 3.1416

# 천 단위 구분
print(f"{big:,}")           # 1,234,567,890
print(f"{big:_}")           # 1_234_567_890

# 퍼센트
ratio = 0.8567
print(f"{ratio:.1%}")       # 85.7%

# 진법 변환
n = 255
print(f"{n:b}")   # 11111111 (2진)
print(f"{n:o}")   # 377      (8진)
print(f"{n:x}")   # ff       (16진)

# 패딩과 정렬
for i in range(1, 4):
    print(f"{i:>5}: {'■' * i}")
알아두면 좋은 점

= 채움 문자와 정렬을 결합하면 f"{val:*>10}"처럼 커스텀 패딩이 가능합니다.

자주 하는 실수

f"{val:,}"는 float에도 적용되지만, 소수점 이하에도 쉼표가 적용될 수 있으므로 f"{val:,.2f}"처럼 명시하세요.

13날짜/시간

datetime 모듈로 날짜와 시간을 다루는 기본 방법입니다. 생성, 포맷, 연산을 포함합니다.

Python code

from datetime import datetime, date, timedelta, timezone

# 현재 시간
now = datetime.now()
print(f"현재: {now:%Y-%m-%d %H:%M:%S}")

# 특정 날짜 생성
birthday = date(2000, 3, 15)
print(f"생일: {birthday:%Y년 %m월 %d일}")

# 날짜 연산
delta = date.today() - birthday
print(f"태어난 지 {delta.days}일")

future = datetime.now() + timedelta(days=30, hours=5)
print(f"30일 5시간 후: {future:%Y-%m-%d %H:%M}")

# UTC 시간대
utc_now = datetime.now(timezone.utc)
kst = timezone(timedelta(hours=9))
kst_now = utc_now.astimezone(kst)
print(f"한국 시간: {kst_now:%H:%M:%S %Z}")
알아두면 좋은 점

시간대를 다룰 때는 항상 timezone-aware datetime을 사용하세요. naive datetime은 시간대 변환 시 버그의 원인이 됩니다.

자주 하는 실수

datetime.now()는 시간대 정보가 없는 naive datetime을 반환합니다. 서버 간 통신에는 datetime.now(timezone.utc)를 사용하세요.

14enum 열거형

Enum으로 관련된 상수를 그룹화하여 타입 안전성과 가독성을 높입니다.

Python code

from enum import Enum, auto, IntEnum

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

# 사용법
print(Color.RED)          # Color.RED
print(Color.RED.name)     # RED
print(Color.RED.value)    # 1

# 비교와 반복
if Color.RED is Color.RED:
    print("같은 멤버입니다")

for c in Color:
    print(f"{c.name}: {c.value}")

# IntEnum: 정수 비교 가능
class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

print(Priority.HIGH > Priority.LOW)  # True
print(Priority.HIGH == 3)            # True
알아두면 좋은 점

auto()는 값을 자동 할당합니다. 값이 중요하지 않은 경우 사용하면 편리합니다.

자주 하는 실수

일반 Enum은 정수와 비교할 수 없습니다. 정수 비교가 필요하면 IntEnum을 사용하세요.

15타입 별칭

Python 3.10+의 TypeAlias와 3.12+의 type 문으로 복잡한 타입에 별칭을 부여합니다.

Python code

from typing import TypeAlias

# Python 3.10+ TypeAlias
Vector: TypeAlias = list[float]
Matrix: TypeAlias = list[Vector]

# 복잡한 타입에 별칭 부여
JsonValue: TypeAlias = (
    str | int | float | bool
    | None | list["JsonValue"]
    | dict[str, "JsonValue"]
)

def scale(v: Vector, factor: float) -> Vector:
    return [x * factor for x in v]

def transpose(m: Matrix) -> Matrix:
    return [list(row) for row in zip(*m)]

# Python 3.12+ type 문 (더 간결)
# type Point = tuple[float, float]
# type Callback = Callable[[int], str]

v: Vector = [1.0, 2.0, 3.0]
print(scale(v, 2))  # [2.0, 4.0, 6.0]
알아두면 좋은 점

타입 별칭은 복잡한 제네릭 타입을 읽기 쉽게 만들어 코드 리뷰 시 이해도를 높입니다.

자주 하는 실수

단순 할당 Vector = list[float]은 런타임에 실제 리스트를 만들 수 있으므로 TypeAlias를 명시하세요.

16TypeVar 제네릭

TypeVar로 제네릭 함수와 클래스를 만들어 타입 안전한 코드를 작성합니다.

Python code

from typing import TypeVar, Generic

T = TypeVar('T')
N = TypeVar('N', int, float)  # 제한된 TypeVar

# 제네릭 함수
def first(items: list[T]) -> T:
    return items[0]

print(first([1, 2, 3]))       # 1 (int)
print(first(["a", "b"]))      # "a" (str)

# 제한된 TypeVar
def add_nums(a: N, b: N) -> N:
    return a + b

print(add_nums(1, 2))       # 3
print(add_nums(1.5, 2.3))   # 3.8

# 제네릭 클래스 (Python 3.12+는 class Box[T]: 가능)
class Box(Generic[T]):
    def __init__(self, item: T) -> None:
        self.item = item
    def get(self) -> T:
        return self.item

box = Box(42)
print(box.get())  # 42
알아두면 좋은 점

Python 3.12+에서는 def first[T](items: list[T]) -> T: 구문으로 더 간결하게 작성할 수 있습니다.

자주 하는 실수

TypeVar("T")의 문자열 인자는 반드시 변수명과 같아야 합니다. T = TypeVar("U")는 혼란을 일으킵니다.

17Literal 타입

Literal로 함수 인자에 허용되는 구체적인 값을 제한하여 타입 안전성을 높입니다.

Python code

from typing import Literal, get_args

Mode = Literal["read", "write", "append"]

def open_file(path: str, mode: Mode) -> str:
    return f"{path}를 {mode} 모드로 열기"

# 정상 호출
print(open_file("data.txt", "read"))

# 타입 체커가 오류 감지 (런타임은 통과)
# open_file("data.txt", "delete")  # mypy 에러!

# Literal 값 추출
print(get_args(Mode))  # ('read', 'write', 'append')

# Literal과 오버로드 결합
from typing import overload

@overload
def process(action: Literal["count"]) -> int: ...
@overload
def process(action: Literal["list"]) -> list: ...

def process(action):
    if action == "count":
        return 42
    return [1, 2, 3]

print(process("count"))  # 42
알아두면 좋은 점

get_args(Mode)로 Literal에 정의된 모든 허용 값을 튜플로 추출할 수 있습니다.

자주 하는 실수

Literal은 런타임에 값을 검증하지 않습니다. 런타임 검증이 필요하면 별도 조건문을 추가하세요.

18Final 상수

Final로 재할당 불가 변수를 선언하여 실수로 값이 변경되는 것을 방지합니다.

Python code

from typing import Final, final

# Final 변수 — 재할당 금지
MAX_RETRIES: Final = 3
API_URL: Final[str] = "https://api.example.com"

# mypy가 다음을 에러로 감지
# MAX_RETRIES = 5  # Cannot assign to final name

# Final과 클래스
class Config:
    DEBUG: Final = False
    VERSION: Final[str] = "1.0.0"

    # @final 데코레이터 — 오버라이드 금지
    @final
    def get_version(self) -> str:
        return self.VERSION

# @final 클래스 — 상속 금지
@final
class Singleton:
    _instance = None

print(f"MAX_RETRIES={MAX_RETRIES}")
print(f"VERSION={Config.VERSION}")
알아두면 좋은 점

Final은 상수 의도를 명확히 하고 타입 체커가 실수를 잡아줍니다. ALL_CAPS 관례와 함께 사용하세요.

자주 하는 실수

Final은 런타임에 재할당을 막지 않습니다. 실제 불변성은 타입 체커(mypy)에서만 보장됩니다.

19유니온 타입

Python 3.10+의 | 연산자로 여러 타입을 허용하는 유니온 타입을 간결하게 표현합니다.

Python code

# Python 3.10+ 파이프 문법
def process(value: int | str) -> str:
    if isinstance(value, int):
        return f"숫자: {value * 2}"
    return f"문자: {value.upper()}"

print(process(5))        # 숫자: 10
print(process("hello"))  # 문자: HELLO

# Optional은 X | None 의 단축
def find_user(user_id: int) -> str | None:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

result = find_user(1)
if result is not None:
    print(result.upper())  # ALICE

# isinstance에서도 사용 (3.10+)
def check(val: object) -> str:
    if isinstance(val, int | str):
        return f"int 또는 str: {val}"
    return "기타 타입"

print(check(42))       # int 또는 str: 42
print(check([1, 2]))   # 기타 타입
알아두면 좋은 점

X | NoneOptional[X]와 동일하지만 더 직관적입니다. Python 3.10+에서는 파이프 문법을 권장합니다.

자주 하는 실수

Python 3.9 이하에서는 int | str 문법을 사용할 수 없습니다. Union[int, str]을 사용하세요.

20walrus 연산자

Python 3.8+의 := (바다코끼리) 연산자로 표현식 안에서 변수에 값을 할당합니다.

Python code

# while 루프에서 활용
import re

data = ["apple 3", "banana 5", "", "cherry 2"]
results = []

for line in data:
    if match := re.match(r"(\w+) (\d+)", line):
        name, count = match.groups()
        results.append((name, int(count)))

print(results)  # [('apple', 3), ('banana', 5), ('cherry', 2)]

# 리스트 컴프리헨션에서 중복 계산 방지
values = [1, 4, 9, 16, 25]
filtered = [sqrt for v in values
            if (sqrt := v ** 0.5) > 2]
print(filtered)  # [3.0, 4.0, 5.0]

# any/all과 함께
names = ["Alice", "Bob", "Charlie"]
if found := next((n for n in names if len(n) > 4), None):
    print(f"긴 이름 발견: {found}")  # Alice
알아두면 좋은 점

walrus 연산자는 값 계산과 조건 검사를 한 줄에 처리할 때 유용합니다.

자주 하는 실수

:=를 남용하면 가독성이 떨어집니다. 단순 할당에는 일반 =을 사용하세요.

21f-string 심화

f-string의 고급 기능으로 표현식, 포맷 스펙, 디버깅 출력, 중첩 등을 다룹니다.

Python code

import datetime

name = "Python"
version = 3.12

# 디버깅 출력 (3.8+)
print(f"{name = }")          # name = 'Python'
print(f"{version = :.1f}")   # version = 3.1

# 표현식과 메서드 호출
items = [1, 2, 3]
print(f"합계: {sum(items)}")
print(f"대문자: {name.upper()!r}")  # 'PYTHON'

# 변환 플래그
val = "hello\nworld"
print(f"{val!r}")   # repr: 'hello\nworld'
print(f"{val!s}")   # str:  hello\nworld
print(f"{val!a}")   # ascii

# 중첩 f-string (3.12+)
width = 10
align = ">"
print(f"{'hello':{align}{width}}")  #      hello

# 날짜 포맷
now = datetime.datetime.now()
print(f"현재: {now:%Y년 %m월 %d일}")
알아두면 좋은 점

f"{x = }" 디버깅 구문에 포맷 스펙을 추가할 수 있습니다: f"{x = :.2f}".

자주 하는 실수

f-string 안에서 백슬래시(\)를 직접 사용할 수 없습니다. 변수에 먼저 할당하거나 chr()을 사용하세요.

22언패킹 심화

*** 언패킹의 고급 활용법입니다. 변수 할당, 함수 호출, 병합에 사용됩니다.

Python code

# 확장 언패킹
first, *middle, last = [1, 2, 3, 4, 5]
print(first, middle, last)  # 1 [2, 3, 4] 5

# 중첩 언패킹
data = ("Alice", (90, 85, 92))
name, (math, eng, sci) = data
print(f"{name}: 수학={math}")

# 딕셔너리 병합 (3.9+)
defaults = {"color": "red", "size": 10}
custom = {"size": 20, "weight": "bold"}
merged = {**defaults, **custom}
print(merged)  # {'color': 'red', 'size': 20, 'weight': 'bold'}

# 3.9+ 파이프 연산자
merged2 = defaults | custom
print(merged2)

# 함수 인자 언패킹
def greet(name, age, city):
    print(f"{name}({age})은 {city}에 삽니다")

info = {"name": "Alice", "age": 30, "city": "서울"}
greet(**info)
알아두면 좋은 점

* 언패킹은 이터러블을 리스트로, **는 딕셔너리를 키워드 인자로 풀어줍니다.

자주 하는 실수

*middle은 항상 리스트를 반환합니다. 빈 경우에도 []이므로 None 체크 대신 길이를 확인하세요.

23딕셔너리 심화

딕셔너리의 고급 메서드와 패턴을 다룹니다. setdefault, get, 딕셔너리 뷰 등을 포함합니다.

Python code

# setdefault: 키가 없으면 기본값 설정 후 반환
groups: dict[str, list] = {}
for name, dept in [("Alice", "Dev"), ("Bob", "Dev"), ("Carol", "HR")]:
    groups.setdefault(dept, []).append(name)
print(groups)  # {'Dev': ['Alice', 'Bob'], 'HR': ['Carol']}

# dict comprehension으로 변환
prices = {"apple": 1000, "banana": 500, "cherry": 2000}
expensive = {k: v for k, v in prices.items() if v >= 1000}
print(expensive)  # {'apple': 1000, 'cherry': 2000}

# 딕셔너리 뷰 활용
a = {"x": 1, "y": 2, "z": 3}
b = {"y": 20, "z": 30, "w": 40}
common_keys = a.keys() & b.keys()
print(common_keys)  # {'y', 'z'}

# dict.fromkeys()
defaults = dict.fromkeys(["a", "b", "c"], 0)
print(defaults)  # {'a': 0, 'b': 0, 'c': 0}

# __missing__ 활용
class AutoDict(dict):
    def __missing__(self, key):
        self[key] = type(self)()
        return self[key]
알아두면 좋은 점

딕셔너리 뷰(.keys(), .items())는 집합 연산을 지원하여 교집합, 차집합 등을 구할 수 있습니다.

자주 하는 실수

dict.fromkeys(keys, [])는 모든 키가 같은 리스트 객체를 공유합니다. 독립된 리스트가 필요하면 컴프리헨션을 사용하세요.

24세트 연산

set의 집합 연산으로 교집합, 합집합, 차집합, 대칭차를 효율적으로 수행합니다.

Python code

a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}

# 기본 집합 연산
print(a & b)   # 교집합: {4, 5}
print(a | b)   # 합집합: {1, 2, 3, 4, 5, 6, 7, 8}
print(a - b)   # 차집합: {1, 2, 3}
print(a ^ b)   # 대칭차: {1, 2, 3, 6, 7, 8}

# 부분집합 / 상위집합
small = {1, 2}
print(small <= a)      # True (부분집합)
print(a >= small)      # True (상위집합)
print(a.isdisjoint(b)) # False (교집합 존재)

# 세트 컴프리헨션
text = "hello world"
unique_vowels = {c for c in text if c in "aeiou"}
print(unique_vowels)  # {'e', 'o'}

# 변경 가능 연산
a.add(10)
a.discard(99)   # 없어도 에러 안 남
a.update([20, 30])
print(a)
알아두면 좋은 점

discard()는 없는 원소를 제거해도 에러가 발생하지 않지만, remove()KeyError를 발생시킵니다.

자주 하는 실수

빈 세트는 set()으로 만들어야 합니다. {}는 빈 딕셔너리입니다.

25frozenset 불변 세트

frozenset은 변경 불가능한 세트로, 딕셔너리 키나 다른 세트의 원소로 사용할 수 있습니다.

Python code

# frozenset 생성
fs = frozenset([1, 2, 3, 4, 5])
print(fs)  # frozenset({1, 2, 3, 4, 5})

# 집합 연산은 가능 (새 frozenset 반환)
fs2 = frozenset([4, 5, 6])
print(fs & fs2)  # frozenset({4, 5})
print(fs | fs2)  # frozenset({1, 2, 3, 4, 5, 6})

# 딕셔너리 키로 사용 가능
permissions = {
    frozenset(["read"]): "viewer",
    frozenset(["read", "write"]): "editor",
    frozenset(["read", "write", "admin"]): "admin",
}
user_perms = frozenset(["read", "write"])
print(permissions[user_perms])  # editor

# 세트의 세트
set_of_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozenset([1, 2]) in set_of_sets)  # True
알아두면 좋은 점

설정값이나 권한 같은 변경되면 안 되는 집합 데이터에 frozenset을 사용하면 안전합니다.

자주 하는 실수

frozensetadd()remove()를 호출하면 AttributeError가 발생합니다.

26네임드튜플

namedtuple로 필드명이 있는 불변 튜플을 만들어 코드 가독성을 높입니다.

Python code

from collections import namedtuple
from typing import NamedTuple

# collections 방식
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(f"x={p.x}, y={p.y}")
print(f"거리: {(p.x**2 + p.y**2)**0.5:.2f}")

# typing 방식 (타입 힌트 포함)
class User(NamedTuple):
    name: str
    age: int
    email: str = "없음"  # 기본값

u = User("Alice", 30)
print(f"{u.name} ({u.age}) - {u.email}")

# _asdict(), _replace()
d = u._asdict()
print(d)  # {'name': 'Alice', 'age': 30, 'email': '없음'}

u2 = u._replace(age=31)
print(u2)  # User(name='Alice', age=31, email='없음')

# 언패킹 지원
name, age, email = u
print(name, age)
알아두면 좋은 점

대부분의 경우 typing.NamedTuple을 사용하세요. 타입 힌트와 기본값을 지원하며 더 읽기 좋습니다.

자주 하는 실수

NamedTuple은 불변이므로 필드 값을 직접 변경할 수 없습니다. 변경이 필요하면 dataclass를 사용하세요.

27Counter 카운터

Counter로 요소의 빈도를 세고, 가장 많이 등장하는 항목을 찾습니다.

Python code

from collections import Counter

# 빈도 세기
text = "abracadabra"
counter = Counter(text)
print(counter)  # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

# 가장 흔한 요소
print(counter.most_common(3))  # [('a', 5), ('b', 2), ('r', 2)]

# 산술 연산
c1 = Counter("aabb")
c2 = Counter("abcc")
print(c1 + c2)  # Counter({'a': 3, 'b': 3, 'c': 2})
print(c1 - c2)  # Counter({'a': 1, 'b': 1})

# 단어 빈도 분석
words = "the cat sat on the mat the cat".split()
word_count = Counter(words)
for word, count in word_count.most_common():
    print(f"{word:>5}: {'■' * count} ({count})")

# total() (Python 3.10+)
print(f"총 글자 수: {counter.total()}")
알아두면 좋은 점

Counterdict의 서브클래스이므로 모든 딕셔너리 메서드를 사용할 수 있습니다.

자주 하는 실수

Counter에서 존재하지 않는 키를 조회하면 KeyError 대신 0을 반환합니다. 이를 의도하지 않으면 주의하세요.

28defaultdict

defaultdict는 존재하지 않는 키에 접근할 때 자동으로 기본값을 생성합니다.

Python code

from collections import defaultdict

# 리스트 기본값
grouped = defaultdict(list)
for name, score in [("수학", 90), ("영어", 85), ("수학", 95), ("영어", 92)]:
    grouped[name].append(score)
print(dict(grouped))  # {'수학': [90, 95], '영어': [85, 92]}

# int 기본값 (0)
counter = defaultdict(int)
for char in "hello world":
    counter[char] += 1
print(dict(counter))

# set 기본값
tags = defaultdict(set)
tags["python"].add("언어")
tags["python"].add("프로그래밍")
tags["java"].add("언어")
print(dict(tags))

# 커스텀 팩토리
def default_user():
    return {"name": "unknown", "active": False}

users = defaultdict(default_user)
print(users["new_user"])  # {'name': 'unknown', 'active': False}
알아두면 좋은 점

defaultdict를 JSON 직렬화할 때는 dict()로 변환하세요. json.dumps가 직접 처리하지 못할 수 있습니다.

자주 하는 실수

defaultdict는 키에 접근만 해도 기본값이 생성됩니다. in으로 존재 여부만 확인할 때는 일반 dict를 사용하세요.

29OrderedDict

OrderedDict는 삽입 순서를 보장하며, 순서 관련 추가 기능을 제공합니다. Python 3.7+에서 일반 dict도 순서를 보장하지만 차이점이 있습니다.

Python code

from collections import OrderedDict

# OrderedDict 고유 기능
od = OrderedDict()
od["c"] = 3
od["a"] = 1
od["b"] = 2

# move_to_end: 항목을 끝으로 이동
od.move_to_end("c")
print(list(od.keys()))  # ['a', 'b', 'c']

# move_to_end(last=False): 처음으로 이동
od.move_to_end("b", last=False)
print(list(od.keys()))  # ['b', 'a', 'c']

# popitem: LIFO (기본) 또는 FIFO
print(od.popitem())           # ('c', 3) 마지막
print(od.popitem(last=False)) # ('b', 2) 처음

# 순서가 같아야 동등
od1 = OrderedDict([("a", 1), ("b", 2)])
od2 = OrderedDict([("b", 2), ("a", 1)])
print(od1 == od2)  # False (순서 다름)

# 일반 dict는 순서 무시
print({"a": 1, "b": 2} == {"b": 2, "a": 1})  # True
알아두면 좋은 점

LRU 캐시 구현 시 OrderedDictmove_to_end()가 유용합니다.

자주 하는 실수

Python 3.7+ dict도 순서를 보장하지만, 동등 비교에서 순서를 고려하지 않습니다. 순서 기반 비교가 필요하면 OrderedDict를 사용하세요.

정리하며

  • 타입 힌트는 실행을 막지 않습니다. 검사 효과를 보려면 mypy·pyright를 CI에 붙여야 합니다.
  • match의 점 없는 이름은 캡처 패턴입니다. 상수 비교는 Enum.MEMBER 형태로 적습니다.
  • 집계 코드는 수동 초기화 대신 Counter·defaultdict로 줄이되 읽기만 할 땐 get()을 씁니다.
  • 전체를 메모리에 올릴 필요가 없으면 리스트 컴프리헨션 대신 제너레이터 표현식을 고릅니다.

더 깊이 들어가고 싶다면 Python 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.