Python Class vs Object: Key Differences and Use
python class vs object: Understand the practical difference between a Python class and an object, how instances are created, and when to use classes over plain data st...
python class vs object requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Python, the distinction between a class and an object is not just a matter of terminology. A class is a blueprint for creating objects, but at runtime a class is itself an object with its own type, attributes, and methods. Understanding the practical difference between a Python class and an object is essential for writing code that uses the language's object model correctly.
What a Class Defines at Definition Time
When you write a class statement, Python executes it and creates a new object of type type. That object holds the class body's namespace: methods, class attributes, and any nested classes. The class object is callable, and calling it produces an instance of that class.
class User: role = "member" def __init__(self, name): self.name = name def greet(self): return f"Hello, {self.name}"
Here User is a class object. It has an attribute role and two function objects, __init__ and greet. The class object itself is not a user; it is the template that defines what a User instance looks like and how it behaves.
Creating Instances: The __init__ Method and Beyond
Calling User("Alice") triggers a sequence of steps. First, Python calls __new__ to allocate a new instance, then calls __init__ to initialize it. The result is an object whose type is User. Each instance has its own namespace, usually stored in __dict__, where instance attributes like name live.
alice = User("Alice") bob = User("Bob") print(alice.name) # Alice print(bob.name) # Bob print(alice.role) # member
alice and bob are distinct objects. They share the class object User, but their name attributes are independent. The role attribute is not stored on the instance; it is found on the class when accessed.
Class Attributes vs Instance Attributes: Where Data Lives
A class attribute is defined directly in the class body and is shared by all instances. An instance attribute is assigned to self (or directly to the instance) and belongs only to that instance. This distinction matters when you mutate an attribute.
alice.role = "admin" # creates an instance attribute, does not change User.role print(User.role) # member print(alice.role) # admin print(bob.role) # member
Assigning to alice.role shadows the class attribute for that instance. The class attribute remains unchanged. This behavior is a common source of confusion, especially when a mutable class attribute is modified in place.
class Team: members = [] # shared mutable list team_a = Team() team_b = Team() team_a.members.append("Alice") print(team_b.members) # ['Alice']
Because members is a class attribute, both instances see the same list. If you intended each team to have its own list, you should assign it in __init__.
Methods Are Functions Attached to a Class
Methods defined in a class are functions that receive the instance as the first argument, conventionally named self. When you call alice.greet(), Python passes alice as self. The method itself is stored on the class, not on the instance.
print(User.greet) # <function User.greet at 0x...> print(alice.greet) # <bound method User.greet of <__main__.User object at 0x...>>
Accessing a method through an instance creates a bound method object that remembers the instance. Accessing it through the class gives the raw function. This distinction is important when passing methods as callbacks; a bound method keeps the instance alive and carries its state.
Class methods and static methods change this behavior. @classmethod receives the class as the first argument, while @staticmethod receives neither the instance nor the class. These are useful for factory functions or utility operations that do not need instance state.
How Python Identifies an Object's Type: type() and isinstance()
Every object in Python has a type, accessible via type(). For an instance, type(alice) returns User. For the class itself, type(User) returns type. The isinstance() function checks whether an object is an instance of a class or a subclass of it.
print(type(alice)) # <class '__main__.User'> print(type(User)) # <class 'type'> print(isinstance(alice, User)) # True print(isinstance(User, type)) # True
isinstance() respects inheritance, so it returns True for subclasses. This is usually the correct way to check an object's type in a polymorphic context. Using type(obj) == SomeClass is stricter and fails for subclasses, which is rarely what you want.
Object Identity and Equality: When Two Objects Are the Same
Two instances of the same class are distinct objects even if they have identical attribute values. The is operator compares identity, while == compares equality, which by default also compares identity unless the class overrides __eq__.
alice1 = User("Alice") alice2 = User("Alice") print(alice1 is alice2) # False print(alice1 == alice2) # False (default behavior)
If you need value-based equality, define __eq__ (and usually __hash__) in the class. Without that, two objects with the same data are not considered equal. This distinction affects how objects behave in sets, dictionaries, and when compared in tests.
Memory and Attribute Lookup: What Happens Under the Hood
Each Python instance typically has a __dict__ that stores its instance attributes. This dictionary adds memory overhead per instance. For classes with many instances and a fixed set of attributes, defining __slots__ can replace the per-instance dictionary with a more compact structure.
class Point: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y
Using __slots__ prevents the creation of __dict__ for each instance, reducing memory usage. However, it also prevents adding new attributes that are not listed, and it can complicate inheritance if not used carefully.
Attribute lookup follows a specific order: the instance's __dict__, then the class, then base classes. This is why class attributes are visible on instances unless shadowed. Understanding this order helps you predict how attribute access behaves and why changing a class attribute affects all instances that have not overridden it.
Choosing Between a Class and a Simpler Data Structure
Not every collection of related data needs to be a class. A dict, a namedtuple, or a dataclass may be simpler and more appropriate depending on the use case.
- A plain
dictis flexible but has no attribute access, no methods, and no type safety. - A
namedtupleprovides immutable, attribute-accessible fields with low overhead, but it is not designed to have methods or mutable state. - A
dataclass(Python 3.7+) generates__init__,__repr__, and equality methods automatically, making it a good middle ground when you need a data container with some behavior.
from dataclasses import dataclass @dataclass class Product: sku: str price: float ```n Use a full class when you need encapsulation, inheritance, or complex behavior tied to the data. Use a simpler structure when you just need to group values and the overhead of a class adds no value. The decision often comes down to whether the object has invariants that must be maintained. If you need to enforce that a price is never negative, a class with validation in `__init__` is appropriate. If you are only passing a set of values around, a `namedtuple` or `dataclass` keeps the code concise without sacrificing clarity.