파이썬 사전(dictionary) 완벽 가이드

사전(dictionary)은 파이썬에서 리스트(list)와 더불어 가장 널리 사용되는 내장 자료형입니다. 하지만 너무 기본적인 기능이다 보니 오히려 이 중요한 자료형의 사용법을 제대로 익히지 않고 넘어가기 쉬운 것 같아요.

이번 포스팅에서는 파이썬에서 사전을 어떻게 사용하는지에 대해서 차근차근 알아보도록 하겠습니다.

참고로 리스트(list)에 대해서는 별도의 포스팅에서 아주 자세히 다루고 있습니다!

사전의 특징

해시 테이블(hash table)이라는 자료구조를 기반으로 하는 사전은 키(key)와 값(value)으로 이루어진 여러 쌍의 데이터를 담기 위해서 사용하는데요. 파이썬의 리스트처럼 가변(mutable) 자료형이기 때문에 사전을 생성 후에 자유롭게 새로운 키에 값을 추가하거나 기존 키의 값을 변경 또는 삭제할 수 있습니다.

동적 언어인 파이썬에서는 키로 해시가 가능한(hashable) 모든 데이터를 사용할 수 있고, 값에 대해서는 아무런 제한없이 어떤 자료형의 데이터도 저장할 수 있는데요. 자바(Java)와 같이 선언할 때 명시한 자료형의 키와 값만 사용하도록 제한하는 정적 타이핑 언어와 비교했을 때 상당히 대조되는 부분입니다.

같은 동적 언어 계열인 자바스크립트(JavaScript)에 익숙하시다면 자연스럽게 객체(object)를 떠올리시게 되실텐데요. 자바스크립트의 객체는 값으로 아무런 데이터나 저장할 수 있다는 측면에서는 파이썬의 사전과 유사하지만, 자바스크립트의 객체는 키로 문자열이나 숫자, 심볼(symbol)만 사용할 수 있다는 점에서 약간의 차이점이 있겠습니다.

사전과 비슷한 특징을 가지되 담고 있는 데이터가 불변(immutable)해야 한다면 파이썬 3.15에 추가된 frozendict나 named tuple을 사용하면 됩니다. 이 부분은 관련 포스팅을 참고 바랍니다.

사전 생성

사전을 생성하는 데는 여러 가지 방법이 있지만, 가장 많이 사용되는 방법은 중괄호({})를 사용하는 것인데요. 중괄호 안에 <키>: <값> 형태의 데이터 쌍을 쉼표(,)로 구분해서 나열해주면 됩니다.

예를 들어, 사용자의 정보를 담아두기 위한 사전은 다음과 같이 생성할 수 있습니다.

>>> { "name": "사용자", "email": "user@test.com", "age": 25 }
{'name': '사용자', 'email': 'user@test.com', 'age': 25}

사실 빈 사전을 생성할 때도 자주 있는데요. 가변 자료형이라 나중에 언제든지 데이터를 추가할 수 있기 때문입니다. 빈 사전을 생성할 때는 빈 중괄호를 사용하거나, dict() 내장 함수를 호출하면 됩니다.

>>> {}
{}
>>> dict()
{}

자주 쓰이는 방법은 아니지만, dict() 내장 함수의 키워드 인자를 사용하거나, 키와 값을 담은 튜플(tuple)의 리스트를 인자로 넘길 수도 있습니다.

>>> dict(name="사용자", email="user@test.com", age=25)
{'name': '사용자', 'email': 'user@test.com', 'age': 25}
>>> dict([("name", "사용자"), ("email", "user@test.com"), ("age", 25)])
{'name': '사용자', 'email': 'user@test.com', 'age': 25}

데이터 추가

사전에 데이터를 추가할 때는 대괄호(사전[<키>] = <값>)를 사용하여 원하는 값을 할당해주면 됩니다.

>>> user = {}
>>> user["name"] = "사용자"
>>> user["email"] = "user@test.com"
>>> user["age"] = 25
>>> user
{'name': '사용자', 'email': 'user@test.com', 'age': 25}

데이터 접근

사전이 담고 있는 데이터에 접근할 때도 대괄호(사전[<키>])를 사용합니다.

>>> user["name"]
'사용자'
>>> user["email"]
'user@test.com'
>>> user["age"]
25

한 가지 주의할 점은 사전에 존재하지 않는 키를 사용하면 KeyError 오류가 발생한다는 것입니다.

>>> user["mail"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'mail'

이러한 오류를 방지하고 싶다면, 사전의 get(<키>[, <기본값>]) 메서드를 사용하면 됩니다. 기본값을 지정해주지 않으면, 키가 존재하지 않을 때, None을 반환합니다.

>>> user.get("mail", "서울")
'서울'
>>> user.get("mail") == None
True

사전에 어떤 키가 존재하는지 안 하는지 확실하지 않을 때는 대괄호 대신에 get() 메서드를 사용하는 것이 좀 더 안전한 데이터 접근이 될 것입니다.

데이터 갱신

가변 자료형인 사전은 자유롭게 담고 있는 데이터를 갱신할 수 있습니다. 기존 키에 새로운 값을 할당하기만 하면 기존 값이 새로운 값으로 대체됩니다.

>>> user["age"] = 31
>>> user
{'name': '사용자', 'email': 'user@test.com', 'age': 31}

데이터 삭제

del 키워드를 사용해서 특정 키와 값의 쌍을 사전에서 제거할 수 있습니다.

>>> del user["email"]
>>> user
{'name': '사용자', 'age': 31}

사전 순회

in 연산자를 통해서 사전에 있는 모든 데이터를 for 루프문으로 순회할 수 있는데요. 기본적으로는 키만 얻어지기 때문에, 값은 대괄호를 이용해서 접근해야 합니다.

>>> user = {"name": "사용자", "email": "user@test.com", "age": 25}
>>> for key in user:
...     print(f"{key}: {user[key]}")
...
name: 사용자
email: user@test.com
age: 25

하지만 사전의 items() 메서드를 활용하면 키와 값을 튜플의 형태로 한 번에 얻을 수 있어서 좀 더 깔끔하게 루프문을 작성할 수 있습니다.

>>> user = {"name": "사용자", "email": "user@test.com", "age": 25}
>>> for key, value in user.items():
...     print(f"{key}: {value}")
...
name: 사용자
email: user@test.com
age: 25

키 존재 여부 확인

in 연산자를 사용하면 특정 키가 사전에 존재하는지도 쉽게 확인 할 수 있습니다.

>>> user = {"name": "사용자", "email": "user@test.com", "age": 25}
>>> "email" in user
True
>>> "mail" in user
False

따라서 if 조건문과도 자연스럽게 함께 사용할 수 있습니다.

>>> if "email" in user:
...     print("email 키는 존재합니다.")
... else:
...     print("email 키를 사전에서 찾을 수 없습니다.")
...
email 키는 존재합니다.

사전 병합

여러 개의 사전을 합쳐야할 때는 ** 연산자를 사용하여, 중괄호 안에 합칠 사전들을 쉼표(,)로 구분하여 나열하면 됩니다. (자바스크립트 개발자라시면 ... 연산자가 떠오르실 거에요 😉)

>>> dic1 = {"A": 1, "B": 2}
>>> dic2 = {"B": 3, "C": 4}
>>> { **dic1, **dic2 }
{'A': 1, 'B': 3, 'C': 4}

Python 3.9 버전부터는 대신에 | 연산자를 사용해서 좀 더 깔끔하게 여러 개의 사전을 병합할 수 있습니다.

>>> dic1 | dic2
{'A': 1, 'B': 3, 'C': 4}

사전 반영

하나의 사전에 다른 사전의 모든 데이터를 반영하고 싶을 때는 사전의 update() 메서드를 사용할 수 있습니다.

>>> dic1 = {"A": 1, "B": 2}
>>> dic2 = {"B": 3, "C": 4}
>>> dic1.update(dic2)
>>> dic1
{'A': 1, 'B': 3, 'C': 4}

바로 위에서 다룬 단순 병합과 달리 기존 사전에 있는 데이터에 직접적인 변경이 가해지는 점을 주의 바라겠습니다.

마찬가지로 Python 3.9 버전에서 사전에 추가된 | 연산자를 활용해서 같은 효과를 낼 수도 있습니다.

>>> dic1 = {"A": 1, "B": 2}
>>> dic2 = {"B": 3, "C": 4}
>>> dic1 |= dic2
>>> dic1
{'A': 1, 'B': 3, 'C': 4}

TypedDict로 키별 타입 지정하기

API 응답처럼 정해진 키가 들어 있는 사전을 함수끼리 주고받을 때, 타입 힌트(type hint)를 dict[str, Any]로만 붙여 두면 아쉬운 점이 많은데요. 어떤 키가 있어야 하는지 알려 주지 않으니, user["nmae"] 같은 오타는 실행해 봐야 KeyError로 드러납니다.

이럴 때는 typing 모듈의 TypedDict를 상속하는 클래스를 만들고, 키 이름과 값 타입을 적어 주면 됩니다.

basic.py
from typing import TypedDict


class User(TypedDict):
    id: int
    name: str
    email: str


def greet(user: User) -> str:
    return f"안녕하세요, {user['nmae']}님!"


user1: User = {"id": "1", "name": "Dale", "email": "dale@example.com"}
user2: User = {"id": 1, "name": "Dale"}

Pyright 같은 타입 검사기(type checker)로 검사해 보면 오타가 난 키, 잘못된 값 타입, 빠뜨린 키를 모두 짚어 냅니다.

uvx pyright basic.py
결과
  basic.py:11:22 - error: Could not access item in TypedDict
    "nmae" is not a defined key in "User" (reportGeneralTypeIssues)
  basic.py:14:22 - error: Type "dict[str, str]" is not assignable to declared type "User"
    "Literal['1']" is not assignable to "int" (reportAssignmentType)
  basic.py:15:15 - error: Type "dict[str, int | str]" is not assignable to declared type "User"
    "email" is required in "User" (reportAssignmentType)
3 errors, 0 warnings, 0 informations

다만 TypedDict는 타입 검사기만을 위한 정보라서, 실행 중에는 평범한 사전과 다르지 않다는 점을 꼭 기억하세요. User(id="1", name="Dale")처럼 값 타입이 틀리거나 키가 빠져도 예외 없이 사전이 만들어집니다. 특히 json 모듈의 json.loads()는 Any를 반환하기 때문에 user: User = json.loads(...)처럼 타입을 붙여도 타입 검사기가 그냥 통과시키니, 외부에서 들어온 데이터는 따로 검증해야 합니다.

있을 수도 있고 없을 수도 있는 키는 NotRequired로 감싸면 됩니다(파이썬 3.11 이상). 반대로 대부분의 키가 선택이라면 클래스에 total=False를 넘기고 필수인 키만 Required로 감쌉니다. 파이썬 3.13부터는 ReadOnly로 감싼 키에 값을 다시 넣는 것도 타입 검사기가 막아 줍니다.

from typing import NotRequired, ReadOnly, Required, TypedDict


class User(TypedDict):
    id: ReadOnly[int]
    name: str
    nickname: NotRequired[str]


class UserUpdate(TypedDict, total=False):
    id: Required[int]
    name: str
    nickname: str

NotRequired로 지정한 키를 user["nickname"]처럼 대괄호로 바로 꺼내면 Pyright가 경고하니, get() 메서드를 쓰거나 in으로 먼저 확인하는 습관을 들이면 좋습니다.

파이썬 3.15: closed와 extra_items

지금까지 본 TypedDict에는 의외의 특징이 하나 있습니다. 정의하지 않은 키가 더 들어 있는 사전도 같은 타입으로 취급될 수 있다는 점인데요.

closed.py
from typing import TypedDict


class Point(TypedDict):
    x: int
    y: int


class ClosedPoint(TypedDict, closed=True):
    x: int
    y: int


class Point3D(TypedDict):
    x: int
    y: int
    z: str


def total(p: Point) -> int:
    return sum(p.values())


def total_closed(p: ClosedPoint) -> int:
    return sum(p.values())


p: Point3D = {"x": 1, "y": 2, "z": "위"}
total(p)
total_closed(p)

Point3D는 x와 y 키가 모두 있으니 Point를 받는 total() 함수에 넘겨도 됩니다. 이렇게 정의에 없는 키가 섞여 들어오기도 하니 타입 검사기는 Point의 값이 전부 int라고 확신하지 못하고, sum()에 넘기는 21번째 줄에서 오류를 냅니다.

파이썬 3.15에서 추가된 closed=True를 붙이면 정의한 키 외에는 아무 키도 없다고 선언하는 것이라서 값이 전부 int라는 사실이 보장됩니다(PEP 728). 그래서 같은 코드인 25번째 줄은 통과하고, 대신 z 키가 있는 Point3D를 넘기는 30번째 줄이 오류가 됩니다.

결과 (일부 생략)
  closed.py:21:12 - error: No overloads for "sum" match the provided arguments (reportCallIssue)
  closed.py:21:16 - error: Argument of type "dict_values[str, object]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
  closed.py:30:14 - error: Argument of type "Point3D" cannot be assigned to parameter "p" of type "ClosedPoint" in function "total_closed"
3 errors, 0 warnings, 0 informations

반대로 정해진 키 몇 개에 더해 임의의 키를 받아야 할 때는 extra_items 인자에 추가 키의 값 타입을 넘깁니다.

extra.py
from typing import TypedDict


class Labels(TypedDict, extra_items=str):
    name: str


labels: Labels = {"name": "web", "env": "prod", "team": "platform"}
bad: Labels = {"name": "web", "replicas": 3}

env와 team처럼 정의하지 않은 키도 값이 문자열이면 통과하고, 값이 정수인 replicas만 걸러집니다.

결과
  extra.py:9:43 - error: Type "dict[str, str | int]" is not assignable to declared type "Labels"
    "Literal[3]" is not assignable to "str" (reportAssignmentType)
1 error, 0 warnings, 0 informations

이 결과는 Pyright 1.1.414로 확인했습니다. Mypy 2.4.0은 아직 extra_items를 지원하지 않습니다. 파이썬 3.14 이하에서 이 기능을 쓰고 싶다면 typing_extensions 패키지에서 TypedDict를 가져오면 됩니다.

전체 코드

본 포스팅에서 제가 작성한 전체 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

https://dales.link/pge

마치며

이상으로 사전(dictionary) 자료형의 특징과 사용 방법에 대해서 자세히 살펴보았는데요. 이정도만 숙지하시면 실전에서 큰 부족함없이 사전을 활용하실 수 있으실 거라 생각합니다. 파이썬에서 정말 광범위하게 사용되는 내장 자료형인 만큼 이번 기회에 사용법을 잘 정리해두셨으면 좋겠습니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord