Classes & Objects
A class is a blueprint for creating objects that bundle data and behavior together - understanding the class/instance relationship is the foundation for everything else in Python's object-oriented programming.
A class defines a new type: a template describing what attributes and methods its instances will have. Creating an instance from a class produces an independent object with its own state, following the shape the class defines. Since 'everything is an object' in Python, classes themselves are objects too (instances of type), but this topic focuses on the everyday mechanics of defining classes and creating instances.
Defining a Class
A class is defined with the class keyword followed by a name (conventionally CapWords/PascalCase) and a body containing attribute assignments and method definitions.
Dog is a class (a blueprint); species lives on the class; name and age are set per-instance inside __init__.
class Dog:
species = 'Canis familiaris' # class attribute - shared by all instances
def __init__(self, name: str, age: int) -> None:
self.name = name # instance attribute - unique per instance
self.age = age
def bark(self) -> str:
return f'{self.name} says Woof!'Creating Instances (Object Creation)
Calling a class like a function creates a new instance: Python allocates a new object and calls __init__ on it to set up its initial state (the full mechanics - __new__ vs __init__ - are covered in the Constructors & __init__ topic).
rex and fido are two distinct instances of Dog, each with independent name/age values.
rex = Dog('Rex', 3)
fido = Dog('Fido', 5)
print(rex.name, rex.age) # Rex 3
print(fido.name, fido.age) # Fido 5
print(type(rex)) # <class '__main__.Dog'>
print(isinstance(rex, Dog)) # TrueAttributes: Instance State
Instance attributes hold the state unique to each object. They're typically set in __init__ via self.attr = value, but Python allows adding, reading, or reassigning attributes on an instance at any time (unless the class restricts this with __slots__).
Each instance normally stores its attributes in its own __dict__, which is why changing rex never affects fido.
rex.age = 4 # reassign an existing attribute
rex.trained = True # add a brand-new attribute, not defined in __init__
print(rex.__dict__) # {'name': 'Rex', 'age': 4, 'trained': True}
print(fido.__dict__) # {'name': 'Fido', 'age': 5} - fido is unaffectedMethods
A method is a function defined inside a class body. When called on an instance, Python automatically passes that instance as the first argument, conventionally named self (see the Instance Methods topic for the full mechanics of binding).
rex.bark() is shorthand that Python expands to Dog.bark(rex) - the instance is implicitly supplied as self.
print(rex.bark()) # 'Rex says Woof!'
print(Dog.bark(rex)) # same call, written explicitly through the classObject Creation Flow (Overview)
At a high level: Dog('Rex', 3) looks up the Dog class, creates a new, mostly-empty object of that type, runs __init__ on it to populate its state, and returns the fully initialized object bound to rex. Every instance keeps a reference back to its class (accessible via type(obj) or obj.__class__), which is how method lookup and isinstance() work.
print(rex.__class__ is Dog) # True
print(rex.__class__ is fido.__class__) # True - same class, different instancesClasses and Objects Are Both Objects
In Python, everything is an object - including classes themselves. A class is an instance of type (its metaclass), which is why you can inspect, pass around, or even dynamically create classes at runtime.
The mechanics of type() as a metaclass, and writing custom metaclasses, go beyond this topic's scope - see a dedicated Metaclasses topic if you need to customize class creation itself.
print(type(Dog)) # <class 'type'> - Dog itself is an instance of type
print(type(rex)) # <class '__main__.Dog'> - rex is an instance of DogPractical Usage: Modeling Real-World Entities
Classes are most useful when you have multiple related pieces of data that travel together and behavior that operates specifically on that data - e.g. a BankAccount bundling a balance with deposit/withdraw logic, rather than passing a balance number around to loose functions.
class BankAccount:
def __init__(self, owner: str, balance: float = 0.0) -> None:
self.owner = owner
self.balance = balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError('deposit amount must be positive')
self.balance += amount
def withdraw(self, amount: float) -> None:
if amount > self.balance:
raise ValueError('insufficient funds')
self.balance -= amount
account = BankAccount('Alice', 100.0)
account.deposit(50)
account.withdraw(30)
print(account.balance) # 120.0Edge Cases: Mutable Default Traps and Shared State
A class attribute assigned a mutable object (like a list) is shared by all instances unless each instance shadows it with its own instance attribute - a very common source of bugs for beginners.
Because members is a class attribute holding a mutable list, and no instance ever assigns its own members, every instance sees the same underlying list object.
class Team:
members = [] # DANGER: one list shared by every Team instance
def add(self, name: str) -> None:
self.members.append(name)
a = Team()
b = Team()
a.add('Alice')
print(b.members) # ['Alice'] - unexpectedly shared!Comparing Instances
By default, == on instances of a plain class falls back to identity comparison (same as is) unless the class defines __eq__. Two instances with identical attribute values are NOT automatically equal.
Defining __eq__ (or using a dataclass, which generates it for you) is how you opt into value-based equality - this is a topic of its own, not covered further here.
d1 = Dog('Rex', 3)
d2 = Dog('Rex', 3)
print(d1 == d2) # False - different objects, no __eq__ defined
print(d1 == d1) # True - same objectKey Takeaways
A class is a blueprint; an instance (object) is a concrete thing built from that blueprint, with its own independent state.
Instance attributes (set via self.attr) belong to one object; class attributes are shared across all instances unless shadowed.
Calling a class like a function creates and initializes a new instance.
Methods automatically receive the instance as their first argument (self) when called on an instance.
Classes are objects too - instances of type - which is why the object model is uniform throughout Python.
class Team: members = [] # shared mutable class attribute def add(self, name): self.members.append(name) class Team: def __init__(self) -> None: self.members = [] # each instance gets its own list def add(self, name: str) -> None: self.members.append(name)
Mutable objects as class attributes are shared by every instance. Initialize mutable per-instance state inside __init__ instead.
d1 = Dog('Rex', 3) d2 = Dog('Rex', 3) if d1 == d2: print('same dog') # never prints - default equality is identity d1 = Dog('Rex', 3) d2 = Dog('Rex', 3) if d1.name == d2.name and d1.age == d2.age: print('same dog')
Without a custom __eq__, == compares identity, not attribute values. Define __eq__ (or use a dataclass) for value-based comparison, or compare the relevant attributes explicitly.
class Config: def set(self, key, value): setattr(self, key, value) c = Config() c.timeout = '30' # typo: should be an int, but nothing prevents it class Config: def __init__(self, timeout: int = 30) -> None: self.timeout = timeout # type hints document intent; validate if needed
Because Python allows freely adding/setting attributes, typos or wrong-typed values on ad hoc attributes are easy to introduce silently - initialize expected attributes explicitly in __init__.
What is the difference between a class and an instance?
A class is the blueprint/template that defines what attributes and methods its objects will have; an instance is a concrete object created from that blueprint, with its own independent state.
What happens when you call Dog('Rex', 3)?
Python creates a new object of type Dog and calls __init__ on it with the given arguments to set up its initial instance attributes, then returns the fully constructed object.
Why did changing rex.age not affect fido.age in the example?
Because age is an instance attribute stored in each object's own __dict__; rex and fido are separate objects with separate attribute storage.
What's the danger of using a mutable object like a list as a class attribute?
Since a class attribute is shared by the class itself (not copied per instance), all instances that don't shadow it with their own instance attribute see and mutate the very same underlying object, causing unintended shared state.
Does Python's default == compare instances by value or by identity?
By identity (same as is), unless the class defines its own __eq__ method (or uses a mechanism like @dataclass that generates one).
Interview: in what sense is a class itself an object in Python?
A class is an instance of a metaclass (type, by default), so it can be assigned to variables, passed as an argument, inspected with type()/dir(), and even created dynamically - the same uniform object model applies to classes as to their instances.
Can you add a new attribute to an instance that wasn't set in __init__?
Yes, by default Python instances can have attributes added, changed, or removed dynamically at any time (unless the class restricts this, e.g. via __slots__).
Write a class Rectangle with width and height instance attributes and a method area() that returns width * height.
class Rectangle: def __init__(self, width: float, height: float) -> None: self.width = width self.height = height def area(self) -> float: return self.width * self.height r = Rectangle(4, 5) print(r.area()) # 20 area() reads self.width and self.height from the specific instance it's called on and returns their product.
What does this print, and why?
3 total is a class attribute incremented via Counter.total (not self.total, so it's not shadowed per-instance); each of the three instantiations increments the shared class attribute, and c3.total reads that shared value since c3 has no instance attribute of its own named total.
Write a class Stack with push, pop, and is_empty methods backed by an instance-level list.
class Stack: def __init__(self) -> None: self._items: list = [] def push(self, item) -> None: self._items.append(item) def pop(self): if self.is_empty(): raise IndexError('pop from empty stack') return self._items.pop() def is_empty(self) -> bool: return len(self._items) == 0 s = Stack() s.push(1) s.push(2) print(s.pop()) # 2 print(s.is_empty()) # False
_items is initialized fresh in __init__ so each Stack instance has its own independent backing list; push/pop/is_empty operate on that per-instance state.
class Counter:
total = 0
def __init__(self):
Counter.total += 1
Counter()
Counter()
c3 = Counter()
print(c3.total)