이번 주에도 무심코 썼을지 모르는 7가지 Python Anti-Patterns (그리고 깔끔한 해결책)
요약 한 줄: Python은 친절하지만, 겉보기에 예쁜 구문 속에 성능 폭탄과 미묘한 버그가 숨어 있어요. 아래 7가지만 손봐도 코드가 확 달라집니다.
Overview
Python은 읽기 쉽고 강력한 batteries-included 언어죠. 그만큼 한 줄로 많은 걸 해버리는 syntactic sugar가 많고, 그 달콤함이 때로는 time complexity나 correctness를 망가뜨리기도 합니다.
이 글에서는 현업 코드 리뷰에서 정말 자주 보는 7가지 anti-patterns를 깔끔히 정리했습니다.
각 항목마다
- 왜 위험한지,
- 어디서 자주 나타나는지,
- 어떻게 고치면 좋은지,
- 체크리스트로 습관화하는 팁
까지 담았습니다. 잔소리가 아니라, 팀과 미래의 나를 편하게 만들자는 마음으로요. 🙂

Pattern 1 — “예쁜 한 줄”이 숨기는 나쁜 time complexity
한눈에 보기 좋은 표현이 실제로는 O(n²) 을 만들어내는 경우가 많습니다.
리스트/문자열 이어 붙이기(반복)에서 터지는 비용
# ❌ loop 안에서 list concatenation
result = []
for x in range(n):
result = result + [x] # 매번 새 list 생성 + 복사 → 누적 O(n²)
해결: append / extend / 한 번만 더하기
# ✅ append: amortized O(1) → 전체 O(n)
result = []
for x in range(n):
result.append(x)
# ✅ 범위면 아예 한 방에
result = list(range(n))
# ✅ 두 list를 한 번만 합칠 땐 +
merged = a + b
string도 마찬가지입니다.
# ❌ loop에서 +=
out = ""
for part in parts:
out += part
# ✅ join
out = "".join(parts)
membership test를 잘못된 자료구조로
# ❌ list에 대한 in 검사 반복 → O(n) * 반복 = O(n²)
for item in candidates:
if item in seen_list:
...
# ✅ set/dict로 바꾸기 → 평균 O(1)
seen = set(seen_list)
for item in candidates:
if item in seen:
...
Quick check
- 문자열을 모아서 하나로? → "".join(parts)
- list가 커지는 루프? → append or comprehension
- in 검사를 여러 번? → set/dict로 캐시
Pattern 2 — ==로 비교해야 할 걸 is로, 혹은 그 반대로
Python에는 두 질문이 있어요.
- Equality: == → __eq__ 호출
- Identity: is → 같은 객체인지(싱글턴 포함)
None 비교는 항상 identity를 씁니다.
# ❌
if value == None:
...
# ✅
if value is None:
...
# ✅
if value is not None:
...
왜냐고요? ==는 사용자 정의 클래스가 __eq__를 이상하게 구현해도 그대로 믿기 때문이죠.
Booleans의 경우엔 보통 truthiness가 가장 Pythonic 합니다.
# ✅
if flag:
...
if not flag:
...
is True/is False는 정말 그 singleton만 허용하고 싶을 때에만 드물게 사용하세요.
작은 데모:
class Weird:
def __eq__(self, other):
return True
x = Weird()
print(x == None) # True (헉)
print(x is None) # False (정상)
Pattern 3 — 쓸 수 있는데 안 쓰는 comprehension / generator expression
list/dict/set comprehension은 코드가 짧아질 뿐 아니라, 종종 단순 loop보다 빠르기도 합니다.
# ❌
squares = []
for num in numbers:
squares.append(num * num)
# ✅
squares = [num * num for num in numbers]
# ✅ filter + transform
evens = [x for x in numbers if x % 2 == 0]
# ✅ dict comprehension
name_to_len = {name: len(name) for name in names}
# ✅ set comprehension
positives = {x for x in items if x > 0}
# ✅ nested (flatten)
flat = [item for row in matrix for item in row]
# ✅ generator expression (lazy)
total = sum(x * x for x in numbers)
# ✅ inline conditional
labels = ["positive" if x > 0 else "non-positive" for x in items]
읽기 어렵다고 느껴지면 과감히 두 줄로 나누면 됩니다. “짧게 쓰는 것”보다 “한눈에 이해되는 것”이 우선이에요.
Pattern 4 — context manager 없이 리소스 직접 열고 닫기
파일, 소켓, DB connection 같은 건 예외가 터져도 무조건 정리돼야 합니다. 그 일을 자동으로 해주는 게 with가 여는 context manager죠.
# ❌ 수동 open/close
f = open(filename, "r")
try:
data = f.read()
finally:
f.close()
# ✅ 안전하고 짧게
with open(filename, "r") as f:
data = f.read()
여러 리소스도 한 줄에:
with open(src, "r") as a, open(dst, "w") as b:
b.write(a.read())
직접 만들고 싶다면 __enter__/__exit__ 또는 contextlib를 쓰면 됩니다.
Pattern 5 — debugging에만 쓰던 print를 production에도 그대로
print는 즉석 실험용으로는 최고지만, 운영 환경에서는 timestamp, level, logger name, traceback 같은 정보가 생명줄입니다. logging을 기본값으로 합시다.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s: %(message)s",
)
logger = logging.getLogger(__name__)
logger.debug("debug")
logger.info("service started")
logger.warning("low disk space")
try:
1 / 0
except ZeroDivisionError:
logger.exception("failure") # traceback 자동 포함
파일에도 남기려면 handler만 추가:
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
"%(asctime)s %(name)s %(levelname)s: %(message)s"
))
logger.addHandler(file_handler)
Tip
- 라이브러리/서비스는 무조건 logging
- DEBUG/INFO/WARNING/ERROR/CRITICAL로 level 관리
- except 안에서는 logger.exception(...)
Pattern 6 — 표준 라이브러리가 이미 해결한 걸 새로 만들기
필요할 때 꺼내 쓰라고 있는 배터리들, 꼭 써요.
개수 세기 → collections.Counter
# ❌ 수동 집계
freq = {}
for item in items:
freq[item] = freq.get(item, 0) + 1
# ✅ Counter
from collections import Counter
freq = Counter(items)
top3 = freq.most_common(3)
경로/파일 작업 → pathlib
from pathlib import Path
base = Path("/data")
report = base / "reports" / "2025" / "summary.csv"
if report.exists() and report.is_file():
print(report.suffix) # .csv
그 밖에 즐겨 찾기:
- itertools (lazy iteration, 조합)
- functools (lru_cache, 고차 함수)
- dataclasses (보일러플레이트 제거)
“내가 만드는 이 helper, 너무 기본적인데?” 싶으면 먼저 표준 라이브러리에서 이름을 찾아보세요.
Pattern 7 — 모두가 한 번쯤 당하는 mutable default argument
list/dict 같은 mutable 객체를 함수 default로 넣으면, 정의 시점 딱 한 번 만들어지고 모든 호출이 그 한 객체를 공유합니다.
# ❌
def add_item_bad(item, items=[]):
items.append(item)
return items
print(add_item_bad("apple")) # ["apple"]
print(add_item_bad("banana")) # ["apple", "banana"] ← ??? 같은 list 재사용
해결: None을 기본값으로 두고 내부에서 생성
# ✅
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
class에서 dataclasses를 쓰면 default_factory=list를 사용하세요.
체크포인트
- 기본값에 [], {}, set() 쓰지 않기
- 기본값은 None → 내부에서 새로 만들기
- dataclasses면 default_factory
Big-O 초간단 암기장
- O(1): set/dict lookup(평균)
- O(log n): binary search, heap
- O(n): 한 번씩 훑기(append 기반)
- O(n log n): 정렬 등
- O(n²): 중첩 loop, 매번 복사하는 concatenation
정확한 숫자보다 상한을 빠르게 가늠하는 습관이 중요합니다.
Copy-Paste용 미니 레시피
Strings
out = "".join(parts)
Lists
result = []
for x in iterable:
result.append(x)
# 또는
result = [f(x) for x in iterable if predicate(x)]
Membership
allowed = set(allowed_list)
if token in allowed:
...
None / Booleans
if value is None:
...
if value is not None:
...
if flag:
...
File handling
from pathlib import Path
p = Path("/tmp/example.txt")
with p.open("w") as f:
f.write("hello")
Logging
import logging
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s: %(message)s")
log = logging.getLogger(__name__)
log.info("service started")
Mutable defaults
def build(item, items=None):
if items is None:
items = []
items.append(item)
return items
FAQ
Q. 왜 loop에서 list concatenation이 느리죠?
a = a + [x]는 매번 새 list를 만들고 복사합니다. 누적 작업량이 O(n²) 로 커져요. append나 comprehension을 쓰세요.
Q. None 비교는 == 대신 is를 써야 하나요?
네. ==는 __eq__에 속아 넘어갈 수 있습니다. is/is not이 정답.
Q. comprehension이 진짜 loop보다 빠른가요?
경우에 따라 그렇습니다. 내부 루프가 C 레벨로 돌아가고 attribute lookup이 줄어드는 이점이 있어요.
Q. with를 쓰면 close를 안 해도 되나요?
맞아요. 예외가 나도 context manager가 정리합니다.
Q. 운영 환경에선 print 대신 뭘 써요?
logging. timestamp/level/logger/traceback을 한 번에 챙길 수 있어요.
Q. list 빈도 세기를 간단히?
collections.Counter(items) 후 most_common().
Q. mutable default argument 버그 어떻게 피하죠?
기본값은 None, 내부에서 []/{}를 새로 만듭니다. dataclasses면 default_factory.
마무리
Python의 우아함은 때때로 오해를 부르기도 합니다. join이 +=를 이기고, set이 membership에서 빛나며, with가 예외 속에서도 리소스를 지킨다는 작은 원리들을 몸으로 익히면 코드가 훨씬 단단해져요.
PR에 작은 체크리스트를 붙여 보세요. 내일의 디버깅 시간을 오늘 줄일 수 있습니다.