Python Object Model

In Python, everything you touch — numbers, functions, classes, even modules — is an object with an identity, a type, and a value. Understanding this model is the key that unlocks almost every other Python behavior.

Python is often summarized with the phrase "everything is an object." That is not a slogan — it is the literal architecture of the language. An integer is an object. A function is an object. A class is an object (an instance of a metaclass). This section builds the mental model of what an object is, how names relate to objects, and how id(), type(), is, and == let you inspect that relationship.

What Is an Object?

In Python, an object is a region of memory that bundles together three things: an identity (a value that is unique and constant for the object's lifetime), a type (which determines what operations the object supports and how it behaves), and a value (the data it holds, which may or may not be changeable). Every literal you write — 42, 3.14, "hello", [1, 2] — creates an object of some type.

Because everything is an object, functions, classes, modules, and even exceptions can be passed around, stored in variables, put into lists, and inspected at runtime just like numbers or strings.

Numbers, strings, functions, classes, and modules are all objects — they all respond to type() the same uniform way.

def greet():
    return "hi"

class Dog:
    pass

import math

for thing in (42, "hello", greet, Dog, math, Ellipsis):
    print(type(thing), thing)

Names Are References, Not Boxes

A common misconception (often carried over from languages like C) is to think of a variable as a labeled box that holds a value. In Python, a name is instead a reference (a binding) to an object living elsewhere in memory. Assignment does not copy data into a box; it makes a name point at an existing object.

a and b are two names bound to one list object. Mutating through b is visible through a because there is only one object.

This is why Python's assignment model is often called "names refer to objects" rather than "variables hold values" — the object exists independently of any particular name.

a = [1, 2, 3]
b = a          # b now refers to the SAME list object as a
b.append(4)
print(a)       # [1, 2, 3, 4]  -- a sees the change too

Identity, Type, and Value

Every object has three attributes you can inspect directly:

• Identity — a value guaranteed unique and constant for the object's lifetime, retrieved with id(obj). CPython implementation detail: id() returns the object's memory address in the reference CPython implementation; other implementations (PyPy, for example) still guarantee id() is unique and constant, just not tied to a memory address. • Type — determines what operations are valid and how the object behaves, retrieved with type(obj). An object's type is itself an object (a class). • Value — the actual data the object represents. For a mutable object, the value can change over the object's lifetime while its identity stays fixed; for an immutable object, the value never changes.

Mutating the list changes its value but not its identity — it is still the same object.

x = [1, 2]
print(id(x), type(x), x)
x.append(3)
print(id(x), type(x), x)   # same id, same type, new value

type() vs is vs ==

Three tools inspect different facets of the object model, and mixing them up is a frequent source of bugs.

• type(obj) returns the object's class — use it to check *what kind* of object something is. • is compares identity — a is b is true only when a and b refer to the exact same object in memory. • == compares equality via the object's __eq__ method — a == b is true when the objects are considered equal by value, even if they are two distinct objects.

a and b hold equal but distinct lists; a and c are literally the same object.

Use is almost exclusively for singleton checks such as x is None, x is True, or comparing against sentinel objects. For everything else — comparing numbers, strings, or container contents — use ==.

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)   # True  -- same contents
print(a is b)   # False -- two distinct list objects
print(a is c)   # True  -- c is another name for the same object

Everything Is an Object — Including Classes and Functions

Because classes and functions are themselves objects, you can treat them as first-class values: store them in variables or containers, pass them as arguments, return them from other functions, and inspect their attributes.

double is stored in a dict like any other value; type(int) shows that classes are instances of the type 'type' (a metaclass), reinforcing that classes are objects too.

The mechanics of classes-as-objects-of-a-metaclass are covered in depth in the Metaclasses topic; here the point is only that the object model is uniform all the way up.

def double(n):
    return n * 2

operations = {"double": double, "type": type(double)}
print(operations["double"](21))   # 42
print(operations["type"])          # <class 'function'>

print(type(int))    # <class 'type'>  -- int itself is an object, an instance of type

Attributes and Namespaces

Most objects carry attributes — named values attached to the object, accessed with dot notation (obj.attr). Attribute access is itself governed by the object model: looking up obj.attr searches the object's own namespace, then its type's namespace, following the type's method resolution order.

vars(p) exposes the instance's own attribute dictionary; deeper attribute-lookup rules belong to the Classes & Objects and Inheritance topics.

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

p = Point(3, 4)
print(p.x, p.y)       # instance attributes
print(vars(p))        # {'x': 3, 'y': 4} -- the instance's own namespace
print(dir(p.__class__)[:3])  # a peek at class-level attributes/methods

Rules and Behavior: What Determines an Object's Type

An object's type is fixed at creation time and (outside of rare, advanced metaprogramming tricks) does not change during the object's life. The type determines every operation the object supports: whether it can be indexed, iterated, added to another object, called like a function, and so on. Python looks up these behaviors through special ("dunder") methods defined on the type, such as __add__, __getitem__, or __call__.

Operators and built-in syntax are convenient spellings for dunder-method calls defined on the object's type.

print((3).__add__(4))     # 7 -- '+' on ints dispatches to int.__add__
print([1, 2].__getitem__(0))  # 1 -- indexing dispatches to __getitem__

Mental Model

Picture objects as boxes floating in memory, each with a permanent ID tag and a type label. Names are sticky notes you can attach to any box, move between boxes, or stick two notes on the same box. id() reads the tag, type() reads the label, is asks "are these two notes on the same box?", and == asks "do these two boxes look the same inside?"

Two names bound to one object; a third name bound to a separate but equal object.

Quick Reference

id(obj)     -> identity (unique, constant for lifetime)
type(obj)   -> the object's type/class
a is b      -> True iff a and b are the SAME object (identity)
a == b      -> True iff a and b are considered equal by value
vars(obj)   -> the object's own attribute namespace (if it has __dict__)

Key Takeaways

Every value in Python is an object with an identity, a type, and a value — including functions, classes, and modules.

Names are references (bindings) to objects, not boxes that store values directly; assignment binds a name, it does not copy.

id() reports identity, type() reports the class, is compares identity, and == compares value equality via __eq__.

Use is for singleton checks like x is None; use == for comparing ordinary values.

An object's type is fixed at creation and governs every operation it supports, dispatched through dunder methods.

if my_list is []: print("empty") if my_list == []: print("empty") # or, more idiomatically: if not my_list: print("empty")

[] in the condition creates a brand-new empty list object every time; it will never be the SAME object as my_list, so is [] is always False. Use == for value comparison, or truthiness for emptiness checks.

if x is 1000: ...

if x == 1000: ...

Small integers are sometimes cached and interned by CPython (a CPython implementation detail, not a language guarantee), so is may work by accident for small numbers but silently break for larger ones or on other implementations. Always compare numeric values with ==.

What three properties does every Python object have?

Identity (id()), type (type()), and value/state.

What is the difference between a is b and a == b?

is checks whether a and b are literally the same object in memory (identity); == checks whether they are considered equal by value, via the object's __eq__ method.

If a = [1, 2] and b = a, does b.append(3) affect a? Why?

Yes — a and b are two names referring to the same list object, so mutating through either name is visible through the other.

Why should x is 'hello' not be relied on to test string equality?

String interning is a CPython implementation detail and not guaranteed for all strings; two equal strings might or might not be the same object, so identity comparison can give inconsistent results.

In what sense is a Python class itself an object?

A class is an instance of a metaclass (by default type); it has an identity, a type (type(MyClass) is type), and can be stored in variables, passed around, and have attributes just like any other object.

What determines whether obj + other is a valid operation?

The type of obj — specifically whether its type defines a suitable __add__ (or other's type defines __radd__).

Does an object's identity ever change during its lifetime in CPython?

No — id() returns a value that is constant for the object's entire lifetime; only its value may change (if it is mutable) or new objects may be created (if operations look like mutation on immutables).

Write a function same_object(a, b) that returns True if a and b refer to the exact same object, and False otherwise (without using ==).

def same_object(a, b): return a is b x = [1, 2] y = x z = [1, 2] print(same_object(x, y)) # True print(same_object(x, z)) # False

Use the is operator, which directly tests identity.

What does this code print?

True True — short literal strings like "hi" are commonly interned by CPython so a is b often prints True (a CPython detail, not guaranteed), and a == c is True regardless because both strings have equal content.

Write code that demonstrates a function is a first-class object by storing two different functions in a list and calling each one from the list.

def add(a, b): return a + b def sub(a, b): return a - b ops = [add, sub] for op in ops: print(op(5, 3))

Functions can be stored in a list just like any other object and called later via the stored reference: op(5, 3).

a = "hi"
b = "hi"
c = "h" + "i"
print(a is b, a == c)