Python Classes, Objects & Special Methods
How to Use This Page
Section titled “How to Use This Page”| Section | Level | You Will Learn |
|---|---|---|
| Classes vs Objects | Beginner | Blueprint vs thing built from it |
| Attributes and Methods | Beginner | Where data and behavior live |
| Special (dunder) methods | Intermediate | Make your classes work with print, ==, +, len, for, sorted, and more |
| Dynamic attributes | Intermediate | Read and write attributes by name at runtime |
| Traps and complexity | Pro tips | Bugs that bite everyone once |
Classes vs Objects
Section titled “Classes vs Objects”A class is the blueprint. An object (also called an instance) is a real thing built from it.
CLASS (written once) OBJECTS (made as many times as you like)
+-----------------------+ +---------------------+| class Dog | ---> | dog_1 ||-----------------------| | name = "Jack" || __init__(name, age) | | age = 3 || bark() | +---------------------++-----------------------+ +---------------------+ ---> | dog_2 | | name = "Thatcher" | | age = 5 | +---------------------+| Concept | Role | Created With |
|---|---|---|
| Class | Defines what data and behavior objects have | class keyword |
| Object | Holds the actual data, uses the behavior | ClassName(args) |
| Attribute | Variable that stores data | Assignment (self.x = x) |
| Method | Function that acts on the object | def inside the class |
One class can produce many objects. Each object keeps its own data.
Anatomy of a Class
Section titled “Anatomy of a Class”class Dog: # 1. class keyword + PascalCase name species = "French Bulldog" # 2. class attribute (shared by all dogs)
def __init__(self, name, age): # 3. initializer: runs on Dog(...) self.name = name # 4. instance attribute (per dog) self.age = age
def bark(self): # 5. method return f"{self.name} says woof!"
d = Dog("Jack", 3) # 6. create an objectprint(d.bark()) # 7. call a method -> Jack says woof!| Piece | Purpose |
|---|---|
class ClassName: | Declares the class. Use PascalCase. |
def __init__(self, ...) | Initializer. Runs automatically when an object is created. |
self | The specific object being created or used. Naming it self is convention, but always follow it. |
self.name = name | Creates an instance attribute on that object. |
def method(self) | Behavior every object can call. |
What Happens When You Write Dog("Jack", 3)
Section titled “What Happens When You Write Dog("Jack", 3)”-
Python creates a new, empty
Dogobject. -
Python calls
__init__(that_object, "Jack", 3). The new object arrives asself. -
Your
__init__runsself.name = nameandself.age = age, storing data on the object. -
The finished object is returned and bound to your variable (
d).
Build It Yourself
Section titled “Build It Yourself”-
Define the class with
class Name:. -
Add
__init__(self, ...)and set instance attributes onself. -
Add methods. Each takes
selfas its first parameter. -
Create objects with
obj = Name(arg1, arg2). -
Call methods and read attributes with dot notation:
obj.method(),obj.attr.
class Dog: def __init__(self, name, age): self.name = name self.age = age
def bark(self): print(f"{self.name.upper()} says woof woof! I'm {self.age} years old!")
dog_1 = Dog("Jack", 3)dog_2 = Dog("Thatcher", 5)
dog_1.bark() # JACK says woof woof! I'm 3 years old!dog_2.bark() # THATCHER says woof woof! I'm 5 years old!Each object uses the same method with its own data. That is why the two outputs differ.
Objects Have Identity
Section titled “Objects Have Identity”Two objects built from the same class with the same data are still different objects.
class Dog: def __init__(self, name): self.name = name
a = Dog("Jack")b = Dog("Jack")c = a
print(type(a)) # <class '__main__.Dog'>print(isinstance(a, Dog)) # Trueprint(a == b) # False -- different objects (default ==)print(a is b) # False -- different memoryprint(a is c) # True -- c is another name for the same object
c.name = "Rex"print(a.name) # Rex -- a and c are the SAME objecta ----+ +--> [ Dog object name="Rex" ] <- ONE object, two names (a and c)c ----+
b ------> [ Dog object name="Jack" ] <- a DIFFERENT object| Tool | Answers |
|---|---|
type(obj) | What class is this? |
isinstance(obj, Cls) | Is it an instance of Cls? |
a is b | Same object in memory? |
a == b | Equal by value? Defaults to is until you define __eq__(). |
Attributes
Section titled “Attributes”Attributes are variables that belong to an object or to a class. Access them with dot notation.
| Kind | Belongs To | Shared? | Set Where | Accessible Via |
|---|---|---|---|---|
| Instance attribute Per object | One object | No | Usually in __init__ via self | Object only |
| Class attribute Shared | The class | Yes, all instances | Class body | Class or any object |
class Dog: species = "French Bulldog" # class attribute
def __init__(self, name): self.name = name # instance attribute
print(Dog.species) # French Bulldog
dog1 = Dog("Jack")print(dog1.name) # Jackprint(dog1.species) # French Bulldog
dog2 = Dog("Tom")print(dog2.name) # Tomprint(dog2.species) # French BulldogInstance attributes need an object first. Class attributes work straight from the class.
More Than One Instance Attribute
Section titled “More Than One Instance Attribute”class Car: def __init__(self, color, model): self.color = color self.model = model
car_1 = Car("red", "Toyota Corolla")car_2 = Car("green", "Lamborghini Revuelto")
print(car_1.model) # Toyota Corollaprint(car_2.model) # Lamborghini Revueltoprint(car_1.color) # redprint(car_2.color) # greenLookup Order and Shadowing
Section titled “Lookup Order and Shadowing”Python checks the instance first, then the class. Assigning through an instance creates a new instance attribute. It does not change the class attribute.
dog1.species | +--> 1. Look in the instance (vars(dog1)) found? use it | +--> 2. Look in the class (Dog) found? use it | +--> 3. Nothing found AttributeErrorclass Dog: species = "French Bulldog" tricks = [] # shared mutable -- trap
def __init__(self, name): self.name = name
a = Dog("Jack")b = Dog("Tom")
a.species = "Pug" # creates an instance attr that shadows the class attrprint(a.species) # Pugprint(b.species) # French Bulldogprint(Dog.species) # French Bulldogprint(vars(a)) # {'name': 'Jack', 'species': 'Pug'}
a.tricks.append("sit") # mutates the shared listprint(b.tricks) # ['sit']class Dog: def __init__(self, name): self.name = name self.tricks = [] # fresh list for every object
a = Dog("Jack")b = Dog("Tom")a.tricks.append("sit")print(a.tricks) # ['sit']print(b.tricks) # []Mutable Default Argument Trap
Section titled “Mutable Default Argument Trap”class Team: def __init__(self, members=[]): # BAD: one list, created once self.members = members
class SafeTeam: def __init__(self, members=None): self.members = [] if members is None else list(members)
a = Team()b = Team()a.members.append("Ana")print(b.members) # ['Ana'] -- shared default list!
c = SafeTeam()d = SafeTeam()c.members.append("Ana")print(d.members) # []Methods
Section titled “Methods”Methods are functions inside a class. They read or change the object’s own data through self.
class Car: def __init__(self, color, model): self.color = color self.model = model
def describe(self): return f"This car is a {self.color} {self.model}"
car_1 = Car("red", "Toyota Corolla")car_2 = Car("green", "Lamborghini Revuelto")
print(car_1.describe()) # This car is a red Toyota Corollaprint(car_2.describe()) # This car is a green Lamborghini RevueltoMethods That Change State
Section titled “Methods That Change State”class Account: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance += amount return self.balance
def withdraw(self, amount): if amount > self.balance: raise ValueError("Insufficient funds") self.balance -= amount return self.balance
acct = Account("Ana", 100)acct.deposit(50)print(acct.balance) # 150acct.withdraw(500) # ValueError: Insufficient fundsWhat self Really Is
Section titled “What self Really Is”obj.method() is shorthand for Class.method(obj). The object before the dot becomes self.
class Dog: def __init__(self, name): self.name = name
def bark(self): return f"{self.name} says woof!"
d = Dog("Jack")print(d.bark()) # Jack says woof!print(Dog.bark(d)) # Jack says woof! -- identical call, spelled outCommon Beginner Mistakes
Section titled “Common Beginner Mistakes”# 1. Forgetting self in the method signatureclass A: def hello(): # BAD return "hi"# A().hello() -> TypeError: hello() takes 0 positional arguments but 1 was given
# 2. Forgetting "self." when assigningclass B: def __init__(self, name): name = name # BAD: local variable, not an attribute# B("x").name -> AttributeError
# 3. Calling a method without parenthesesclass C: def ping(self): return "pong"
c = C()print(c.ping) # <bound method C.ping of ...> -- the method, not the resultprint(c.ping()) # pong| Symptom | Likely Cause | Fix |
|---|---|---|
takes 0 positional arguments but 1 was given | Missing self in def | Add self as first parameter |
AttributeError on an attribute you just set | Forgot self. | Write self.name = name |
Output shows <bound method ...> | Missing () | Call it: obj.method() |
| Changing one object changes all | Mutable class attribute or default | Create the value in __init__ |
Special Methods (Dunder Methods)
Section titled “Special Methods (Dunder Methods)”Special methods start and end with double underscores. The name is short for double underscore. Python calls them automatically for operators, built-in functions, and language features.
You rarely call them directly. 3 + 4 is clearer than (3).__add__(4).
print(3 + 4) # 7print((3).__add__(4)) # 7 -- what Python runs under the hoodprint(len("abc")) # 3print("abc".__len__()) # 3You write Python runs-------------- ----------------------------------Cls(...) --> Cls.__new__(...), then __init__(...)str(obj) --> obj.__str__()len(obj) --> obj.__len__()obj[i] --> obj.__getitem__(i)x in obj --> obj.__contains__(x)for x in obj --> obj.__iter__(), then __next__() repeatedlya + b --> a.__add__(b)a == b --> a.__eq__(b)a < b --> a.__lt__(b)obj(...) --> obj.__call__(...)bool(obj) --> obj.__bool__(), else obj.__len__() != 0The Families at a Glance
Section titled “The Families at a Glance”| Family | What It Controls | Key Methods |
|---|---|---|
| Creation | Building the object | __init__() |
| Text | How the object prints | __str__(), __repr__(), __format__() |
| Equality and hashing | ==, set, dict keys | __eq__(), __hash__() |
| Ordering | <, >, sorted() | __lt__(), __gt__() |
| Arithmetic | +, -, *, / | __add__(), __sub__(), __mul__(), __truediv__() |
| Size and truthiness | len(), if obj: | __len__(), __bool__() |
| Container access | obj[i], x in obj | __getitem__(), __contains__() |
| Iteration | for x in obj | __iter__(), __next__() |
| Calling | obj(...) | __call__() |
When Python Calls Them
Section titled “When Python Calls Them”| Category | Trigger | Method Called |
|---|---|---|
| Initialization | Cls(...) | __init__() |
| Arithmetic | a + b | __add__() |
| Arithmetic | a - b | __sub__() |
| Arithmetic | a * b | __mul__() |
| Arithmetic | a / b | __truediv__() |
| Text | str(obj), print(obj) | __str__() |
| Text | repr(obj) | __repr__() |
| Text | format(obj, spec) | __format__() |
| Comparison | a == b | __eq__() |
| Comparison | a < b | __lt__() |
| Comparison | a > b | __gt__() |
| Hashing | hash(obj), set, dict keys | __hash__() |
| Truthiness | bool(obj), if obj: | __bool__(), else __len__() |
| Size | len(obj) | __len__() |
| Indexing | obj[i] | __getitem__() |
| Membership | x in obj | __contains__() |
| Iteration | for x in obj | __iter__() |
| Iteration | next(it) | __next__() |
| Calling | obj(...) | __call__() |
Why Custom Classes Need Them
Section titled “Why Custom Classes Need Them”Built-in types already know how to add, compare, and loop. Your own classes do not. Without special methods, Python falls back to defaults:
class Book: def __init__(self, title, pages): self.title = title self.pages = pages
book1 = Book("Built Wealth Like a Boss", 420)book2 = Book("Be Your Own Start", 420)
print(len(book1)) # TypeError: object of type 'Book' has no len()print(str(book1)) # <__main__.Book object at 0x102ed2900>print(book1 == book2) # False -- identity check, not content| Call | Result | Reason |
|---|---|---|
len(book1) | TypeError | No __len__() defined |
str(book1) | Default memory-address text | No __str__() defined |
book1 == book2 | False | Default == compares identity, not content |
Defining Your Own: Size, Text, Equality
Section titled “Defining Your Own: Size, Text, Equality”class Book: def __init__(self, title, pages): self.title = title self.pages = pages
def __len__(self): return self.pages
def __str__(self): return f"'{self.title}' has {self.pages} pages"
def __eq__(self, other): if not isinstance(other, Book): return NotImplemented return self.pages == other.pages
def __hash__(self): return hash(self.pages)
book1 = Book("Built Wealth Like a Boss", 420)book2 = Book("Be Your Own Start", 420)
print(len(book1)) # 420print(str(book1)) # 'Built Wealth Like a Boss' has 420 pagesprint(book1 == book2) # TrueExample: Text Conversion
Section titled “Example: Text Conversion”Two methods turn an object into text. Define the one you need.
| Method | Audience | Called By | Typical Output |
|---|---|---|---|
__str__() | End users | print(), str(), f-strings | (2, 3) |
__repr__() | Developers | repr(), REPL, debugger, items inside a list | Point(2, 3) |
class Point: def __init__(self, x, y): self.x = x self.y = y
def __str__(self): return f"({self.x}, {self.y})"
def __repr__(self): return f"Point({self.x}, {self.y})"
p = Point(2, 3)print(p) # (2, 3) -> __str__print(repr(p)) # Point(2, 3) -> __repr__print([p]) # [Point(2, 3)] -> containers use __repr__Example: Arithmetic Operators (Vector)
Section titled “Example: Arithmetic Operators (Vector)”class Vector: def __init__(self, x, y): self.x = x self.y = y
def __str__(self): return f"({self.x}, {self.y})"
def __add__(self, other): return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other): return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar): return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other): return self.x == other.x and self.y == other.y
def __hash__(self): return hash((self.x, self.y))
a = Vector(1, 2)b = Vector(3, 4)print(a + b) # (4, 6)print(b - a) # (2, 2)print(a * 3) # (3, 6)print(a == Vector(1, 2)) # TrueExample: Comparison and Sorting Advanced
Section titled “Example: Comparison and Sorting ”Define __eq__ and one ordering method such as __lt__. @total_ordering generates the rest. Then sorted(), min(), and max() work.
from functools import total_ordering
@total_orderingclass Card: def __init__(self, rank): self.rank = rank
def __eq__(self, other): return self.rank == other.rank
def __lt__(self, other): return self.rank < other.rank
def __hash__(self): return hash(self.rank)
hand = [Card(9), Card(2), Card(7)]print([c.rank for c in sorted(hand)]) # [2, 7, 9]print(max(hand).rank) # 9print(Card(2) <= Card(7)) # True -- generated by @total_orderingExample: Container Behavior (Shopping Cart)
Section titled “Example: Container Behavior (Shopping Cart)”Regular methods handle actions (add, remove). Special methods make the object behave like a built-in container.
| Method | Enables |
|---|---|
__len__() | len(cart) |
__iter__() | for item in cart |
__contains__() | "Monitor" in cart |
__getitem__() | cart[3] |
class Cart: def __init__(self): self.items = []
def add(self, item): self.items.append(item)
def remove(self, item): if item in self.items: self.items.remove(item) else: print(f"{item} is not in cart")
def list_items(self): return self.items
def __len__(self): return len(self.items)
def __getitem__(self, index): return self.items[index]
def __contains__(self, item): return item in self.items
def __iter__(self): return iter(self.items)
cart = Cart()cart.add("Laptop")cart.add("Wireless mouse")cart.add("Ergo keyboard")cart.add("Monitor")
for item in cart: print(item, end=" ") # Laptop Wireless mouse Ergo keyboard Monitorprint()
print(len(cart)) # 4print(cart[3]) # Monitorprint("Monitor" in cart) # Trueprint("banana" in cart) # False
cart.remove("Ergo keyboard")print(cart.list_items()) # ['Laptop', 'Wireless mouse', 'Monitor']cart.remove("banana") # banana is not in cartBonus: Truthiness and Slicing for Free
Section titled “Bonus: Truthiness and Slicing for Free”# Uses the Cart class abovecart = Cart()print(bool(cart)) # False -- __len__() returns 0if not cart: print("Cart is empty") # runs
cart.add("Laptop")cart.add("Monitor")print(bool(cart)) # Trueprint(cart[0:2]) # ['Laptop', 'Monitor'] -- slice is passed to the listExample: Iteration Protocol Advanced
Section titled “Example: Iteration Protocol ”for n in Countdown(3): | +-- iter(obj) -> obj.__iter__() returns an iterator +-- next(it) -> it.__next__() gives 3, then 2, then 1 +-- StopIteration raised loop ends quietlyclass Countdown: def __init__(self, start): self.current = start
def __iter__(self): return self
def __next__(self): if self.current <= 0: raise StopIteration value = self.current self.current -= 1 return value
for n in Countdown(3): print(n, end=" ") # 3 2 1print()print(list(Countdown(3))) # [3, 2, 1]Example: Callable Objects Handy
Section titled “Example: Callable Objects ”__call__() lets an object be used like a function. It can remember state between calls.
class Multiplier: def __init__(self, factor): self.factor = factor
def __call__(self, value): return value * self.factor
double = Multiplier(2)triple = Multiplier(3)
print(double(5)) # 10print(triple(5)) # 15print(list(map(double, [1, 2, 3]))) # [2, 4, 6] -- works anywhere a function doesMake Your Class Feel Built-in
Section titled “Make Your Class Feel Built-in”-
Add
__init__()to store the data. -
Add
__str__()for output users see, and__repr__()for output developers see. -
Add
__eq__()and__hash__()if two objects with the same data should be equal. -
Add
__lt__()with@total_orderingif objects need sorting. -
Add
__add__()and friends if operators make sense for your data. -
Add
__len__(),__iter__(),__contains__(), or__getitem__()if the object holds items. -
Test with
print(),len(),==,in,for, andsorted().
Skip the Boilerplate: @dataclass
Section titled “Skip the Boilerplate: @dataclass”@dataclass writes __init__, __repr__, and __eq__ for you. Learn the manual versions first so you know what it generates.
from dataclasses import dataclass
@dataclassclass Point: x: int y: int
p = Point(2, 3)print(p) # Point(x=2, y=3)print(p == Point(2, 3)) # True -- compares fieldsDynamic Attribute Handling
Section titled “Dynamic Attribute Handling”Use these built-ins when attribute names are only known at runtime (user input, config files, API payloads).
| Function | Purpose | Missing Attribute |
|---|---|---|
getattr(obj, name, default) Read | Get a value | AttributeError, or default if given |
setattr(obj, name, value) Create / Update | Set a value | Creates it |
hasattr(obj, name) Check | Test existence, returns bool | Returns False |
delattr(obj, name) Delete | Remove an attribute | AttributeError |
dir(obj) Inspect | List all attribute names | n/a |
vars(obj) Inspect | Instance attributes as a dict | n/a |
Static (name fixed in code) Dynamic (name held in a variable)--------------------------- ---------------------------------obj.color getattr(obj, "color")obj.color = "red" setattr(obj, "color", "red")del obj.color delattr(obj, "color")(no equivalent) hasattr(obj, "color")getattr()
Section titled “getattr()”class Person: def __init__(self, name, age): self.name = name self.age = age
person = Person("John Doe", 30)
print(getattr(person, "name")) # John Doeprint(getattr(person, "city", "Milano")) # Milano (default)
attr_name = input("Enter the attribute you want to see: ")print(getattr(person, attr_name, "Attribute not found"))| User Types | Output |
|---|---|
name | John Doe |
age | 30 |
email | Attribute not found |
The regular obj.attribute syntax cannot do this because the name is not fixed in your code.
Real-World Use: Dispatch by Name
Section titled “Real-World Use: Dispatch by Name”class Robot: def move(self): return "moving"
def stop(self): return "stopped"
bot = Robot()
for command in ["move", "stop", "dance"]: action = getattr(bot, command, None) if callable(action): print(action()) # moving, then stopped else: print(f"Unknown command: {command}") # Unknown command: danceListing Attributes: dir() and vars()
Section titled “Listing Attributes: dir() and vars()”dir() returns every attribute name, including dunders and methods. Filter with startswith("__") and callable() to keep only data attributes. vars() is the shortcut for instance data.
class Person: def __init__(self, name, age): self.name = name self.age = age
person = Person("John Doe", 30)
for attr in dir(person): if not attr.startswith("__") and not callable(getattr(person, attr)): print(f"{attr}: {getattr(person, attr)}")
print(vars(person)) # {'name': 'John Doe', 'age': 30}
# Loop output (dir() is alphabetical):# age: 30# name: John Doe| Tool | Returns | Includes Methods and Dunders? |
|---|---|---|
dir(obj) | Sorted list of names | Yes |
vars(obj) | dict of instance data | No |
setattr()
Section titled “setattr()”class Configuration: pass
settings_data = { "server_url": "https://api.example.com", "timeout_sec": 30, "max_retries": 5,}
config_obj = Configuration()
for attr_name, attr_value in settings_data.items(): setattr(config_obj, attr_name, attr_value)
print(config_obj.server_url) # https://api.example.comprint(config_obj.timeout_sec) # 30Real-World Use: Build an Object from JSON
Section titled “Real-World Use: Build an Object from JSON”import json
class Record: def __init__(self, **fields): for key, value in fields.items(): setattr(self, key, value)
payload = json.loads('{"id": 7, "name": "Ana"}')rec = Record(**payload)print(rec.name) # Anaprint(vars(rec)) # {'id': 7, 'name': 'Ana'}hasattr() and delattr()
Section titled “hasattr() and delattr()”Check before you use or delete. delattr() raises AttributeError on a missing name.
class Product: def __init__(self, name, price): self.name = name self.price = price
product = Product("T-Shirt", 25)
for attr in ["name", "price", "inventory_id"]: if hasattr(product, attr): print(f"{attr}: {getattr(product, attr)}") else: print(f"ERROR: missing required attribute '{attr}'")
# name: T-Shirt# price: 25# ERROR: missing required attribute 'inventory_id'class UserSession: def __init__(self, user_id, token): self.user_id = user_id self.auth_token = token # sensitive self.temp_counter = 0 # temporary
session = UserSession(101, "a1b2c3d4e5")
for attr in ["auth_token", "temp_counter"]: if hasattr(session, attr): delattr(session, attr) print(f"Removed attribute: {attr}")
print(hasattr(session, "auth_token")) # Falseprint(vars(session)) # {'user_id': 101}Complexity Cheatsheet
Section titled “Complexity Cheatsheet”| Operation | Cost |
|---|---|
| Create an object | O(1) plus whatever __init__ does |
| Read or write an attribute | O(1) average (dict lookup) |
getattr, setattr, hasattr, delattr | O(1) average |
vars(obj) | O(1) (returns the instance __dict__) |
dir(obj) | O(n log n) (builds a sorted list of names) |
| Method call | O(1) dispatch plus the method body |
Quick Recap
Section titled “Quick Recap”| Need | Use |
|---|---|
| Blueprint for objects | class Name: |
| Per-object data | self.attr = value in __init__ |
| Data shared by all objects | Class attribute (immutable values only) |
| Mutable per-object default | None default, create inside __init__ |
| Readable output for users | __str__() |
| Unambiguous output for developers | __repr__() |
| Value-based equality | __eq__() plus __hash__() |
Sorting and < > | __lt__() plus @total_ordering |
Operators like +, -, * | __add__(), __sub__(), __mul__() |
Work with len(), in, for, [] | __len__(), __contains__(), __iter__(), __getitem__() |
| Custom loop behavior | __iter__() plus __next__() |
| Use an object like a function | __call__() |
| Attribute name known only at runtime | getattr(), setattr(), hasattr(), delattr() |
| Inspect an object | type(), isinstance(), dir(), vars() |
| Skip the boilerplate | @dataclass |