Built-in Data Types

Python ships with a rich set of built-in types out of the box — from None and bool to lists, dicts, and memoryview — each with a distinct role. This topic maps the full landscape before later topics dive into each one individually.

Python's built-in data types cover scalars (numbers, booleans, None), sequences (str, bytes, list, tuple, range), mappings (dict), sets (set, frozenset), binary data views (bytearray, memoryview), and the open-ended category of user-defined objects. This topic is a guided tour of the whole landscape — each type gets a concise treatment here, with full detail reserved for their own dedicated topics.

The Type Landscape at a Glance

Every value in Python belongs to exactly one type at any moment, and that type determines what operations are available. The built-in types fall into natural families:

None type      -> NoneType (the absence of a value)
Booleans       -> bool
Numbers        -> int, float, complex
Text            -> str
Binary data     -> bytes, bytearray, memoryview
Sequences       -> list, tuple, range, str, bytes
Mappings        -> dict
Sets            -> set, frozenset
User-defined    -> classes you write yourself

NoneType

None is the sole instance of NoneType and represents "no value" or "nothing here." It is commonly used as a default placeholder, a function's implicit return value, or a sentinel meaning "not provided."

Always compare against None with is/is not, never ==, since None is a singleton and identity comparison is both correct and faster.

def no_return():
    pass

result = no_return()
print(result)             # None
print(result is None)     # True -- the idiomatic way to check
print(type(None))          # <class 'NoneType'>

bool

bool has exactly two instances, True and False. Under the hood, bool is a subclass of int (True == 1 and False == 0), a historical design choice that lets booleans participate directly in arithmetic while still printing and comparing as booleans.

print(isinstance(True, int))   # True -- bool IS-A int
print(True + True)              # 2
print(sum([True, False, True])) # 2 -- counts the True values

int, float, complex

int represents arbitrary-precision whole numbers, float represents IEEE-754 double-precision numbers, and complex represents numbers with a real and imaginary part. The dedicated Numbers topic covers literals, precision, and pitfalls in depth; here's the minimal shape of each.

i = 10
f = 10.5
c = 3 + 4j
print(type(i), type(f), type(c))
print(c.real, c.imag)   # 3.0 4.0

str, bytes, bytearray, memoryview

str is Python's immutable text type (a sequence of Unicode code points). bytes is its immutable binary counterpart (a sequence of integers 0-255). bytearray is the mutable version of bytes. memoryview provides a zero-copy view over the memory of another binary-supporting object (like bytes or bytearray), letting you slice or inspect raw memory without copying it.

str vs bytes is a strict separation in modern Python: you cannot silently mix text and binary data — encode() converts str to bytes, decode() converts bytes back to str.

text = "caf\u00e9"
data = text.encode("utf-8")     # bytes
buf = bytearray(data)            # mutable copy
buf[0] = ord("C")

view = memoryview(data)
print(bytes(view[:4]))            # first 4 bytes, no copy of the full object made
print(type(text), type(data), type(buf), type(view))

list, tuple, range

list is a mutable, ordered sequence. tuple is its immutable counterpart. range is a memory-efficient, immutable sequence of integers, generated lazily according to a start/stop/step formula rather than stored element-by-element.

nums = [1, 2, 3]           # list -- mutable
coords = (10, 20)           # tuple -- immutable
r = range(0, 10, 2)         # range -- lazy, immutable
print(list(r))               # [0, 2, 4, 6, 8]
print(len(r), r[3])           # 5 6 -- range supports len() and indexing without materializing

dict, set, frozenset

dict is a mutable mapping from unique, hashable keys to values, and preserves insertion order (a language guarantee since Python 3.7). set is a mutable, unordered collection of unique hashable elements, ideal for membership tests and set algebra. frozenset is set's immutable counterpart, which makes it hashable and usable as a dict key or set element.

scores = {"alice": 90, "bob": 85}   # dict
tags = {"python", "web", "python"}   # set -- duplicates collapse
frozen = frozenset(tags)              # frozenset -- hashable
lookup = {frozen: "immutable tag set"}
print(scores, tags, lookup)

User-Defined Objects

Beyond the built-ins, you can define your own types with class. A user-defined object is mutable by default (its instance attributes can be reassigned freely) and, unless you customize __eq__/__hash__, is compared by identity and hashed by its default identity-based hash.

Every class in Python ultimately inherits from object, the root of the type hierarchy — this is another expression of "everything is an object."

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

p = Point(1, 2)
p.x = 99          # mutable by default
print(type(p), isinstance(p, object))  # every class implicitly inherits from object

Quick Reference: Mutability at a Glance

*memoryview itself has no independent value to mutate, but if it wraps a mutable buffer like bytearray, writes through the view mutate the underlying buffer.

Type          Mutable?   Ordered?     Hashable?
NoneType      no         n/a          yes
bool          no         n/a          yes
int/float     no         n/a          yes
complex       no         n/a          yes
str           no         yes          yes
bytes         no         yes          yes
bytearray     yes        yes          no
list          yes        yes          no
tuple         no         yes          yes (if elements are hashable)
range         no         yes          yes
dict          yes        yes (ins.)   no
set           yes        no           no
frozenset     no         no           yes
memoryview    view*      yes          no

Key Takeaways

Python's built-in types cover scalars (NoneType, bool, int, float, complex), text and binary sequences (str, bytes, bytearray, memoryview), general sequences (list, tuple, range), mappings (dict), and sets (set, frozenset).

Each type has a fixed mutability and hashability profile that determines how it can be used, especially as dict keys or set elements.

str and bytes are strictly separated: text vs binary data never mix silently.

User-defined classes extend the type system and, like everything else in Python, are themselves objects, ultimately inheriting from object.

Each of these types has its own dedicated topic elsewhere on the site; this page is the map, not the territory.

if value == None: ...

if value is None: ...

None is a singleton; identity comparison (is) is both idiomatic and immune to a class overriding __eq__ in a way that makes == None misleading.

cache = {} cache[[1, 2]] = "value" # TypeError: unhashable type: 'list' cache = {} cache[(1, 2)] = "value" # tuple is hashable

Mutable types (list, dict, set) are unhashable and cannot be used as dict keys or set elements; use an immutable equivalent like tuple or frozenset instead.

List the built-in scalar (non-container) types covered here.

NoneType, bool, int, float, complex.

Why is bool considered a subtype of int?

bool is implemented as a subclass of int, so True and False behave as 1 and 0 in arithmetic contexts, while isinstance(True, int) is True.

What is the practical difference between bytes and bytearray?

bytes is immutable binary data; bytearray is its mutable counterpart, supporting in-place item assignment and methods like append().

Why can frozenset be used as a dict key while set cannot?

frozenset is immutable and hashable; set is mutable and therefore unhashable, since its hash would be unstable if its contents changed after insertion.

What advantage does range have over a list of the same integers?

range stores only start, stop, and step and computes elements lazily on demand, using constant memory regardless of how many integers it logically represents.

Are user-defined objects mutable or immutable by default?

Mutable by default — their instance attributes can be reassigned freely unless the class specifically restricts this (e.g. frozen dataclasses, read-only properties).

What root type do all Python classes ultimately inherit from?

object — every class, built-in or user-defined, is ultimately a subclass of object.

Write code that builds a dict counting how many times each type appears in a mixed list of values.

values = [1, "a", 2.5, None, True, "b", 3] counts: dict[type, int] = {} for v in values: t = type(v) counts[t] = counts.get(t, 0) + 1 print(counts) type(v) gives each value's exact type, used as a dict key to tally occurrences with counts.get(t, 0) + 1.

What does this code print, and why are the two results different?

True then False -- bool is a subclass of int, so every bool IS an int (isinstance(True, int) is True), but the reverse isn't true: a plain int like 1 is not a bool.

Write a function describe(value) that returns a string naming the value's type and whether it is hashable, using a try/except around hash().

def describe(value): try: hash(value) hashable = True except TypeError: hashable = False return f"{type(value).__name__}: hashable={hashable}" print(describe([1, 2])) # list: hashable=False print(describe((1, 2))) # tuple: hashable=True hash() raises TypeError on unhashable (mutable) types like list, dict, and set; catching that exception detects hashability directly.

print(isinstance(True, int))
print(isinstance(1, bool))