Python Data Types: The Complete Map
Python ships with a small set of built in types. Sorting them by three questions - single value or container, ordered or not, changeable or not - makes the whole set easy to hold in your head.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
Why a map helps
Python has perhaps a dozen built in types that matter day to day. Learning them one by one produces a list to memorise. Learning them by the three questions that actually distinguish them produces understanding, and the list then remembers itself.
- Does it hold one value or many? Scalar or container.
- If many, is the order meaningful? Sequence or unordered collection.
- Can it be changed after creation? Mutable or immutable.
The map
| Type | Holds | Ordered | Mutable | Written as |
|---|---|---|---|---|
int | a whole number | - | No | 42 |
float | a decimal number | - | No | 3.5 |
complex | a complex number | - | No | 2 + 3j |
bool | true or false | - | No | True |
str | text | Yes | No | "hello" |
list | any values | Yes | Yes | [1, 2, 3] |
tuple | any values | Yes | No | (1, 2, 3) |
set | unique values | No | Yes | {1, 2, 3} |
frozenset | unique values | No | No | frozenset({1, 2}) |
dict | key to value pairs | Yes, by insertion | Yes | {"a": 1} |
bytes | raw bytes | Yes | No | b"data" |
NoneType | the absence of a value | - | - | None |
Since Python 3.7 a dictionary preserves insertion order as a guaranteed part of the language. Sets do not, and never will: a set has no notion of position at all.
Seeing the type of anything
print(type(42)) # <class 'int'>
print(type(3.5)) # <class 'float'>
print(type("hello")) # <class 'str'>
print(type([1, 2])) # <class 'list'>
print(type((1, 2))) # <class 'tuple'>
print(type({1, 2})) # <class 'set'>
print(type({"a": 1})) # <class 'dict'>
print(type(True)) # <class 'bool'>
print(type(None)) # <class 'NoneType'>The scalar types in one pass
int
Whole numbers of unlimited size. There is no 32 bit or 64 bit ceiling and no overflow; Python grows the number as far as memory allows.
print(2 ** 200) # a 61 digit number, computed exactly
print(1_000_000) # underscores are readability only, the value is 1000000float
Decimal numbers stored in binary, which means they are approximations.
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # FalseThis is not a Python defect. Binary fractions cannot represent 0.1 exactly, in the same way decimal cannot represent one third exactly. For money and anywhere else exactness matters, use decimal.Decimal, covered in the standard library notes.
complex
z = 2 + 3j
print(z.real, z.imag) # 2.0 3.0
print(abs(z)) # 3.605551275463989Rarely needed outside engineering and mathematics, but it is a first class built in type, not an add on.
bool
print(True + True) # 2
print(isinstance(True, int)) # Truebool is a subclass of int, with True equal to 1 and False equal to 0. That is occasionally useful, for example sum(scores > 50 for scores in results) counts how many passed. It also explains some surprising output.
None
result = None
print(result is None) # True, the correct test
# if result == None: # works, but never written this wayNone means "no value here". It is a single object: every None in a program is the same object, which is why is is the right comparison. A function with no return statement returns None.
The container types in one pass
names = ["Meera", "Arun", "Sara"] # list: ordered, changeable, duplicates allowed
point = (12.5, 48.2) # tuple: ordered, fixed, a single record
seen = {"Meera", "Arun"} # set: unordered, unique, fast membership
ages = {"Meera": 27, "Arun": 31} # dict: look a value up by a key
label = "Invoice 2024" # str: an ordered sequence of charactersChoosing between them
| You need to | Use | Because |
|---|---|---|
| Keep items in order and change them | list | Order is preserved and the list can grow. |
| Group a few fields that belong together | tuple | Fixed size, cannot be modified by mistake, usable as a dictionary key. |
| Remove duplicates, or test membership often | set | Membership is roughly constant time regardless of size. |
| Look something up by a name or id | dict | Direct key access instead of scanning. |
| Work with text | str | Immutable, and every text method is already built in. |
Mutable and immutable
This is the distinction that causes the most confusion later, so it deserves stating plainly here.
- Immutable:
int,float,complex,bool,str,tuple,frozenset,bytes,None. - Mutable:
list,set,dict,bytearray, and almost every class you write yourself.
text = "hello"
# text[0] = "H" # TypeError: str does not support item assignment
text = "Hello" # this is a new string, bound to the same name
items = [1, 2, 3]
items[0] = 99 # the list itself changed
print(items) # [99, 2, 3]Only immutable objects can be dictionary keys or set members, because a key that could change would break the lookup. That is the practical reason the distinction exists.
Dynamic typing, one more time
value = 10
print(type(value)) # int
value = "ten"
print(type(value)) # strThe name changed what it points at. Neither object changed type. Types belong to objects, never to names.
Common mistakes
- Comparing floats with
==. Compare with a tolerance, or usemath.isclose. - Writing
{}and expecting an empty set. That is an empty dictionary; an empty set isset(). - Writing
(5)and expecting a tuple. That is the number 5 in brackets; a one element tuple is(5,). - Using
== Noneinstead ofis None. - Trying to use a list as a dictionary key, then being puzzled by
TypeError: unhashable type. - Assuming a set keeps the order you inserted in. It does not.
Best practices
- Pick the container from the operation you perform most, not from habit.
- Prefer a tuple when the collection should not change; it documents intent and protects you.
- Use
isinstance(x, int)rather thantype(x) == int, so subclasses are handled correctly. - Keep money out of
float.
Practice
- For each of these, name the type you would choose and say why in one line: unique visitor ids; a row read from a CSV file; a configuration file loaded into memory; the coordinates of a point; a queue of pending jobs.
- Predict and then explain the output of
print(True + True + False). - Explain why
{"a": 1}and{1, 2}use the same brackets without ambiguity, and what{}means. - Show two different ways to test whether
0.1 + 0.2is close enough to0.3. - Group all the built in types listed above into mutable and immutable from memory, then check yourself.
Conclusion
Three questions - one value or many, ordered or not, changeable or not - place every built in type on the map. Everything that follows in this path, from slicing to dictionary keys to copying, is decided by where a type sits on it.