Python Built-in Data Types: A Practical Reference
python built in data types: Practical guide to Python's built-in data types: behavior, performance characteristics, and when to choose each type for real-world code.
python built in data types requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Python's built-in data types—int, float, str, bool, list, tuple, dict, set, frozenset, bytes, and bytearray—are the primitives behind nearly every Python program. Each type has distinct mutability, memory, and performance characteristics that shape how your code behaves under real workloads. Selecting the wrong type for a collection that grows to thousands of elements can turn an O(1) operation into an O(n) scan, or force avoidable allocations in a hot loop.
The Core Scalar Types: int, float, str, and bool
int in Python is arbitrary precision. It does not overflow at 32 or 64 bits; it grows as needed. This is convenient for calculations that would overflow in languages like C or Java, but it means large integers consume more memory and arithmetic becomes slower as the value grows. For most application code this is irrelevant, but if you are processing millions of large numbers, the cost is real.
float is a double-precision IEEE 754 value. It has the same precision limitations as any double: 0.1 + 0.2 does not equal 0.3 exactly. When you need exact decimal arithmetic, use the decimal module, not float.
# float precision behavior print(0.1 + 0.2) # 0.30000000000000004
str is immutable and stores Unicode code points. Every string operation that appears to modify a string actually creates a new object. Concatenating many strings in a loop is O(n²) because each + creates a new string and copies the previous content. Use ''.join(...) for bulk concatenation.
bool is a subclass of int. True is 1 and False is 0. This means True + True == 2 works, which is occasionally useful but more often a source of subtle bugs when booleans are used in arithmetic contexts.
List vs Tuple: Sequence Types with Different Contracts
list is mutable and resizable. It is implemented as a dynamic array with overallocation, so appending is amortized O(1). Inserting or removing from the middle is O(n) because elements must shift.
tuple is immutable and fixed-size. It uses less memory than a list because it does not need the overallocation buffer or the resize logic. Tuples are also hashable when all their elements are hashable, which makes them usable as dictionary keys or set members.
# tuple as dictionary key coordinates = {(10, 20): "origin", (30, 40): "target"}
Choose a tuple when the sequence length is fixed and the elements represent a single logical unit. Choose a list when the collection grows, shrinks, or is reordered.
dict and set: Hash-Based Types
dict is a hash table mapping keys to values. Since Python 3.6, dicts preserve insertion order. Lookup, insertion, and deletion are average O(1), but worst-case O(n) when many keys collide.
Keys must be hashable. Mutable types like list, dict, and set are not hashable and cannot be used as keys. If you need a list-like key, convert it to a tuple first.
set is a hash table of keys with no associated values. It supports O(1) membership testing, union, intersection, and difference. frozenset is the immutable version and is itself hashable, so it can be used as a dict key or set element.
# set membership and operations active_ids = {101, 102, 103} blocked_ids = {103, 104} available = active_ids - blocked_ids # {101, 102}
Memory for dict and set is larger per element than list because the hash table needs extra slots to keep the load factor low. If you only need ordered iteration over a small collection, a list may be more memory-efficient.
bytes and bytearray: Binary Data Types
bytes is an immutable sequence of integers in the range 0–255. bytearray is its mutable counterpart. Both are used for binary protocols, file I/O, and network data.
bytes objects are hashable and can be used as dict keys. bytearray cannot be hashed because it is mutable.
# reading binary data with open("image.bin", "rb") as f: data = f.read() # bytes
When you need to modify binary data in place, bytearray avoids creating a new object for every change, which matters when processing large buffers.
Performance and Memory Characteristics
The table below summarizes the practical performance characteristics of the main built-in types.
| Type | Mutability | Lookup/Index | Insert/Append | Memory Profile |
|---|---|---|---|---|
int | Immutable | N/A | N/A | Variable, grows with value |
float | Immutable | N/A | N/A | Fixed 8 bytes |
str | Immutable | O(1) index | N/A | 1 byte per ASCII char, more for Unicode |
list | Mutable | O(1) index | Amortized O(1) append | Overallocated buffer |
tuple | Immutable | O(1) index | N/A | Minimal, no overallocation |
dict | Mutable | O(1) average | O(1) average | Hash table with spare slots |
set | Mutable | O(1) average | O(1) average | Hash table with spare slots |
bytes | Immutable | O(1) index | N/A | Compact binary storage |
bytearray | Mutable | O(1) index | Amortized O(1) append | Resizable buffer |
The practical takeaway: for large collections, measure the memory footprint with sys.getsizeof and the time with timeit rather than guessing. The asymptotic complexity is usually the deciding factor, but constant factors matter when you process millions of elements.
Choosing the Right Type for the Job
The decision depends on three questions: is the collection mutable, is order significant, and is membership testing or keyed access the dominant operation?
- Use a
tuplewhen the sequence is fixed and represents a single record, such as coordinates or a version number. - Use a
listwhen you append, remove, or reorder elements. - Use a
dictwhen you need to map keys to values and the key set is known at access time. - Use a
setwhen you only need membership testing and duplicate elimination, and order is irrelevant. - Use
frozensetwhen you need a set that must be hashable, such as a key in another dict.
For example, tracking unique user IDs in a session is a set operation, not a list operation:
# deduplication with set seen = set() for event in event_stream: if event.user_id not in seen: seen.add(event.user_id) process(event)
Common Pitfalls and Edge Cases
Mutable default arguments are the classic Python trap. A default value is evaluated once at function definition time, so a mutable default like [] is shared across all calls.
def add_item(item, cache=[]): cache.append(item) return cache
The fix is to use None as the default and create a fresh list inside the function.
Hashability is another edge case. A tuple containing a list is not hashable, even though the tuple itself is immutable. The hash is computed from the elements, and a mutable element breaks the contract.
When comparing types, remember that == checks value equality, while is checks identity. For small integers and short strings, Python may intern objects, so is can return True for equal values. Relying on this behavior is fragile and should be avoided in production code.
When Type Selection Affects Production Behavior
In a long-running service, the choice between list and set for membership testing is the difference between O(n) and O(1) per check. If you check membership inside a hot loop over thousands of items, the list version degrades quadratically while the set version stays linear.
Similarly, storing large binary payloads as bytearray instead of repeatedly concatenating bytes objects avoids repeated allocation and copying. For network packet processing or file chunking, this is a measurable difference.
The immutable types (tuple, frozenset, bytes) are safe to share across threads without locks because they cannot be mutated. If your service uses a shared configuration or cache keyed by a tuple, you get thread safety for free. Mutable types require explicit synchronization.