Back to Blog
Python

Python Dict Type: Syntax, Typing, and Performance

python dict type: Learn how the Python dict type works, how to type it correctly, and when to use it over other structures.

Pythondicttype hintsdata structuresperformance
Illustration of a Python dictionary mapping keys to values with type annotations.

The python dict type is a hash table that maps keys to values. It is one of the most used data structures in Python, and understanding its runtime behavior and type semantics matters for writing correct and efficient code.

What the Dict Type Is at Runtime

A dict stores key-value pairs in a hash table. When you insert a key, Python computes a hash of the key and uses it to determine the storage location. Lookup, insertion, and deletion have an average time complexity of O(1), assuming the hash function distributes keys evenly.

The runtime representation is not a simple list of pairs. CPython uses a compact layout that separates the hash table from the entries, which reduces memory usage and improves cache locality. This is why the order of keys is preserved: since Python 3.7, dicts maintain insertion order as part of the language specification, not just an implementation detail.

d = {} d['a'] = 1 d['b'] = 2 print(list(d.keys())) # ['a', 'b']

Declaring Dict Types in Function Signatures

Type hints for dicts allow you to specify the expected key and value types. The typing.Dict alias is still common, but modern Python code uses the built-in dict[key_type, value_type] syntax, which works in Python 3.9 and later.

def count_words(text: str) -> dict[str, int]: counts = {} for word in text.split(): counts[word] = counts.get(word, 0) + 1 return counts

The annotation dict[str, int] tells type checkers that the function returns a dict with string keys and integer values. This catches mistakes such as accidentally returning a dict with mixed value types.

For older Python versions, typing.Dict is the equivalent:

from typing import Dict def count_words(text: str) -> Dict[str, int]: # ...

Common Dict Operations and Their Cost

Most dict operations are constant-time on average, but the actual cost depends on the hash function and collision handling. The in operator, get, setdefault, and pop all perform a hash lookup. Iterating over keys or items is O(n).

config = {'host': 'localhost', 'port': 5432} if 'port' in config: print(config['port'])

setdefault is a common pattern for initializing a value only if the key is absent:

visits = {} visits.setdefault('home', 0) visits['home'] += 1

This avoids a separate check and assignment, and it is thread-safe in the sense that the operation is atomic in CPython due to the GIL, but that is not a guarantee across implementations.

When a Dict Is the Wrong Choice

A dict is not always the right data structure. If you need to maintain a sequence of items and access them by index, a list is more appropriate. If you need to associate a value with a key and the key set is fixed and small, a tuple or a simple class may be simpler and faster.

Consider a lookup table for a small set of constants:

STATUS_CODES = { 200: 'OK', n 404: 'Not Found', 500: 'Internal Server Error', }

This is fine. But if you are mapping from an integer range to a same-sized integer range, a list or an array can be more memory-efficient and faster because it avoids hashing.

Type Hints and Runtime Performance

Type hints do not affect runtime performance in CPython. They are stored in the __annotations__ attribute and are not used by the interpreter for optimization. The only runtime cost is the time to parse the annotation at function definition, which is negligible for typical programs.

However, type checkers like mypy or pyright can catch errors before runtime. Using dict[str, list[int]] or dict[tuple[int, int], str] makes the intended structure explicit, which reduces maintenance overhead and helps with refactoring.

One important limitation: dict is invariant in its key and value types. A dict[str, int] is not a subtype of dict[object, int] even though str is a subtype of object. This is because you can put a non-string key into a dict[object, int], which would break the original type assumption.

Handling Missing Keys Without Exceptions

Accessing a missing key raises a KeyError. To avoid this, you have several options. The get method returns a default value:

value = data.get('missing', 0)

The defaultdict from the collections module automatically creates a default value when a key is missing:

from collections import defaultdict word_counts = defaultdict(int) for word in words: word_counts[word] += 1

defaultdict is useful when the default is a simple type, but be careful with mutable defaults. The factory function is called each time a missing key is accessed, so defaultdict(list) creates a new list for each new key.

Dict Ordering and Compatibility Across Python Versions

Insertion order is guaranteed in Python 3.7 and later. In earlier versions, order was an implementation detail of CPython. If you rely on order, you should not assume it on Python 3.6 or older. For code that must run on multiple Python versions, you can use collections.OrderedDict, which preserves order even in older versions and has additional methods like move_to_end.

from collections import OrderedDict ordered = OrderedDict() ordered['a'] = 1 ordered['b'] = 2 ordered.move_to_end('a')

Since Python 3.7, OrderedDict is mostly redundant for simple ordering, but it still provides extra functionality that plain dict lacks.

Memory and Performance Considerations

A dict uses more memory than a list of tuples because it stores hash values and maintains a sparse table. The exact overhead depends on the number of entries and the load factor. When a dict grows, it rehashes and copies all entries, which is an O(n) operation. This is amortized over many insertions, so the average cost remains O(1).

If you are dealing with a very large number of records, consider whether a dict is necessary. For example, if you are counting occurrences of a small set of known strings, a Counter from collections is a specialized dict that may be more convenient, but it does not reduce memory overhead.

Hash collisions can degrade performance. Python uses a randomized hash seed for strings to prevent denial-of-service attacks that exploit predictable collisions. This means that the exact ordering of keys in a dict is not deterministic across runs, even though insertion order is preserved. Do not rely on the internal hash order for any logic.

For numeric keys, the hash is usually the integer itself, so collisions are rare. For custom objects, you should implement __hash__ and __eq__ together. If you define __eq__ without __hash__, the object becomes unhashable, and you cannot use it as a dict key.

class Point: def __init__(self, x, y): self.x = x self.y = y def __hash__(self): return hash((self.x, self.y)) def __eq__(self, other): return isinstance(other, Point) and self.x == other.x and self.y == other.y

This ensures that two points with the same coordinates are considered equal and have the same hash, so they map to the same dict entry.

When performance is critical, measure your actual workload rather than assuming a dict is the bottleneck. Profiling often reveals that the real cost is in the code that builds or iterates the dict, not the lookups themselves. If you need to optimize, consider using sys.intern for repeated string keys, or use a more specialized structure like array for homogeneous numeric data.

A final note: the dict type is mutable, so passing it to a function does not copy it. If you need to avoid modifying the original, you must create a shallow copy with dict.copy() or copy.deepcopy() for nested structures. This is a common source of bugs when a function mutates a dict that was passed in as an argument.

def add_defaults(config: dict[str, int]) -> dict[str, int]: new_config = config.copy() new_config.setdefault('retries', 3) return new_config

Understanding these behaviors of the python dict type helps you write code that is both correct and efficient, and it lets you choose the right data structure for the problem at hand.

python dict type: Practical Usage and Code Examples | RYUSLOG DEV