Skip to content

Python Classes, Objects & Special Methods

SectionLevelYou Will Learn
Classes vs ObjectsBeginnerBlueprint vs thing built from it
Attributes and MethodsBeginnerWhere data and behavior live
Special (dunder) methodsIntermediateMake your classes work with print, ==, +, len, for, sorted, and more
Dynamic attributesIntermediateRead and write attributes by name at runtime
Traps and complexityPro tipsBugs that bite everyone once

A class is the blueprint. An object (also called an instance) is a real thing built from it.

blueprint_vs_objects.txt
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 |
+---------------------+
ConceptRoleCreated With
ClassDefines what data and behavior objects haveclass keyword
ObjectHolds the actual data, uses the behaviorClassName(args)
AttributeVariable that stores dataAssignment (self.x = x)
MethodFunction that acts on the objectdef inside the class

One class can produce many objects. Each object keeps its own data.

anatomy.py
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 object
print(d.bark()) # 7. call a method -> Jack says woof!
PiecePurpose
class ClassName:Declares the class. Use PascalCase.
def __init__(self, ...)Initializer. Runs automatically when an object is created.
selfThe specific object being created or used. Naming it self is convention, but always follow it.
self.name = nameCreates 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)”
  1. Python creates a new, empty Dog object.

  2. Python calls __init__(that_object, "Jack", 3). The new object arrives as self.

  3. Your __init__ runs self.name = name and self.age = age, storing data on the object.

  4. The finished object is returned and bound to your variable (d).

  1. Define the class with class Name:.

  2. Add __init__(self, ...) and set instance attributes on self.

  3. Add methods. Each takes self as its first parameter.

  4. Create objects with obj = Name(arg1, arg2).

  5. Call methods and read attributes with dot notation: obj.method(), obj.attr.

dog_class.py
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.

Two objects built from the same class with the same data are still different objects.

identity.py
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)) # True
print(a == b) # False -- different objects (default ==)
print(a is b) # False -- different memory
print(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 object
names_vs_objects.txt
a ----+
+--> [ Dog object name="Rex" ] <- ONE object, two names (a and c)
c ----+
b ------> [ Dog object name="Jack" ] <- a DIFFERENT object
ToolAnswers
type(obj)What class is this?
isinstance(obj, Cls)Is it an instance of Cls?
a is bSame object in memory?
a == bEqual by value? Defaults to is until you define __eq__().

Attributes are variables that belong to an object or to a class. Access them with dot notation.

KindBelongs ToShared?Set WhereAccessible Via
Instance attribute Per objectOne objectNoUsually in __init__ via selfObject only
Class attribute SharedThe classYes, all instancesClass bodyClass or any object
attributes.py
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) # Jack
print(dog1.species) # French Bulldog
dog2 = Dog("Tom")
print(dog2.name) # Tom
print(dog2.species) # French Bulldog

Instance attributes need an object first. Class attributes work straight from the class.

car_attributes.py
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 Corolla
print(car_2.model) # Lamborghini Revuelto
print(car_1.color) # red
print(car_2.color) # green

Python checks the instance first, then the class. Assigning through an instance creates a new instance attribute. It does not change the class attribute.

attribute_lookup.txt
dog1.species
|
+--> 1. Look in the instance (vars(dog1)) found? use it
|
+--> 2. Look in the class (Dog) found? use it
|
+--> 3. Nothing found AttributeError
shadowing_trap.py
class 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 attr
print(a.species) # Pug
print(b.species) # French Bulldog
print(Dog.species) # French Bulldog
print(vars(a)) # {'name': 'Jack', 'species': 'Pug'}
a.tricks.append("sit") # mutates the shared list
print(b.tricks) # ['sit']
shadowing_fix.py
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.py
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 are functions inside a class. They read or change the object’s own data through self.

methods.py
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 Corolla
print(car_2.describe()) # This car is a green Lamborghini Revuelto
stateful_methods.py
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) # 150
acct.withdraw(500) # ValueError: Insufficient funds

obj.method() is shorthand for Class.method(obj). The object before the dot becomes self.

self_explained.py
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 out
common_mistakes.py
# 1. Forgetting self in the method signature
class A:
def hello(): # BAD
return "hi"
# A().hello() -> TypeError: hello() takes 0 positional arguments but 1 was given
# 2. Forgetting "self." when assigning
class B:
def __init__(self, name):
name = name # BAD: local variable, not an attribute
# B("x").name -> AttributeError
# 3. Calling a method without parentheses
class C:
def ping(self):
return "pong"
c = C()
print(c.ping) # <bound method C.ping of ...> -- the method, not the result
print(c.ping()) # pong
SymptomLikely CauseFix
takes 0 positional arguments but 1 was givenMissing self in defAdd self as first parameter
AttributeError on an attribute you just setForgot self.Write self.name = name
Output shows <bound method ...>Missing ()Call it: obj.method()
Changing one object changes allMutable class attribute or defaultCreate the value in __init__

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).

dunder_under_the_hood.py
print(3 + 4) # 7
print((3).__add__(4)) # 7 -- what Python runs under the hood
print(len("abc")) # 3
print("abc".__len__()) # 3
operator_to_dunder_map.txt
You 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__() repeatedly
a + b --> a.__add__(b)
a == b --> a.__eq__(b)
a < b --> a.__lt__(b)
obj(...) --> obj.__call__(...)
bool(obj) --> obj.__bool__(), else obj.__len__() != 0
FamilyWhat It ControlsKey Methods
CreationBuilding the object__init__()
TextHow 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 truthinesslen(), if obj:__len__(), __bool__()
Container accessobj[i], x in obj__getitem__(), __contains__()
Iterationfor x in obj__iter__(), __next__()
Callingobj(...)__call__()
CategoryTriggerMethod Called
InitializationCls(...)__init__()
Arithmetica + b__add__()
Arithmetica - b__sub__()
Arithmetica * b__mul__()
Arithmetica / b__truediv__()
Textstr(obj), print(obj)__str__()
Textrepr(obj)__repr__()
Textformat(obj, spec)__format__()
Comparisona == b__eq__()
Comparisona < b__lt__()
Comparisona > b__gt__()
Hashinghash(obj), set, dict keys__hash__()
Truthinessbool(obj), if obj:__bool__(), else __len__()
Sizelen(obj)__len__()
Indexingobj[i]__getitem__()
Membershipx in obj__contains__()
Iterationfor x in obj__iter__()
Iterationnext(it)__next__()
Callingobj(...)__call__()

Built-in types already know how to add, compare, and loop. Your own classes do not. Without special methods, Python falls back to defaults:

no_special_methods.py
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
CallResultReason
len(book1)TypeErrorNo __len__() defined
str(book1)Default memory-address textNo __str__() defined
book1 == book2FalseDefault == compares identity, not content
book_special_methods.py
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)) # 420
print(str(book1)) # 'Built Wealth Like a Boss' has 420 pages
print(book1 == book2) # True

Two methods turn an object into text. Define the one you need.

MethodAudienceCalled ByTypical Output
__str__()End usersprint(), str(), f-strings(2, 3)
__repr__()Developersrepr(), REPL, debugger, items inside a listPoint(2, 3)
text_conversion.py
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__
vector_operators.py
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)) # True

Example: 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.

ordering.py
from functools import total_ordering
@total_ordering
class 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) # 9
print(Card(2) <= Card(7)) # True -- generated by @total_ordering

Example: 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.

MethodEnables
__len__()len(cart)
__iter__()for item in cart
__contains__()"Monitor" in cart
__getitem__()cart[3]
cart.py
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 Monitor
print()
print(len(cart)) # 4
print(cart[3]) # Monitor
print("Monitor" in cart) # True
print("banana" in cart) # False
cart.remove("Ergo keyboard")
print(cart.list_items()) # ['Laptop', 'Wireless mouse', 'Monitor']
cart.remove("banana") # banana is not in cart
cart_truthiness.py
# Uses the Cart class above
cart = Cart()
print(bool(cart)) # False -- __len__() returns 0
if not cart:
print("Cart is empty") # runs
cart.add("Laptop")
cart.add("Monitor")
print(bool(cart)) # True
print(cart[0:2]) # ['Laptop', 'Monitor'] -- slice is passed to the list

Example: Iteration Protocol Advanced

Section titled “Example: Iteration Protocol ”
for_loop_protocol.txt
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 quietly
countdown_iterator.py
class 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 1
print()
print(list(Countdown(3))) # [3, 2, 1]

__call__() lets an object be used like a function. It can remember state between calls.

callable_objects.py
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)) # 10
print(triple(5)) # 15
print(list(map(double, [1, 2, 3]))) # [2, 4, 6] -- works anywhere a function does
  1. Add __init__() to store the data.

  2. Add __str__() for output users see, and __repr__() for output developers see.

  3. Add __eq__() and __hash__() if two objects with the same data should be equal.

  4. Add __lt__() with @total_ordering if objects need sorting.

  5. Add __add__() and friends if operators make sense for your data.

  6. Add __len__(), __iter__(), __contains__(), or __getitem__() if the object holds items.

  7. Test with print(), len(), ==, in, for, and sorted().

@dataclass writes __init__, __repr__, and __eq__ for you. Learn the manual versions first so you know what it generates.

dataclass_shortcut.py
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(2, 3)
print(p) # Point(x=2, y=3)
print(p == Point(2, 3)) # True -- compares fields

Use these built-ins when attribute names are only known at runtime (user input, config files, API payloads).

FunctionPurposeMissing Attribute
getattr(obj, name, default) ReadGet a valueAttributeError, or default if given
setattr(obj, name, value) Create / UpdateSet a valueCreates it
hasattr(obj, name) CheckTest existence, returns boolReturns False
delattr(obj, name) DeleteRemove an attributeAttributeError
dir(obj) InspectList all attribute namesn/a
vars(obj) InspectInstance attributes as a dictn/a
static_vs_dynamic.txt
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")
dynamic_getattr.py
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("John Doe", 30)
print(getattr(person, "name")) # John Doe
print(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 TypesOutput
nameJohn Doe
age30
emailAttribute not found

The regular obj.attribute syntax cannot do this because the name is not fixed in your code.

getattr_dispatch.py
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: dance

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.

dynamic_dir_vars.py
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
ToolReturnsIncludes Methods and Dunders?
dir(obj)Sorted list of namesYes
vars(obj)dict of instance dataNo
dynamic_setattr.py
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.com
print(config_obj.timeout_sec) # 30
setattr_from_json.py
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) # Ana
print(vars(rec)) # {'id': 7, 'name': 'Ana'}

Check before you use or delete. delattr() raises AttributeError on a missing name.

dynamic_hasattr.py
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'
dynamic_delattr.py
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")) # False
print(vars(session)) # {'user_id': 101}
OperationCost
Create an objectO(1) plus whatever __init__ does
Read or write an attributeO(1) average (dict lookup)
getattr, setattr, hasattr, delattrO(1) average
vars(obj)O(1) (returns the instance __dict__)
dir(obj)O(n log n) (builds a sorted list of names)
Method callO(1) dispatch plus the method body
NeedUse
Blueprint for objectsclass Name:
Per-object dataself.attr = value in __init__
Data shared by all objectsClass attribute (immutable values only)
Mutable per-object defaultNone 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 runtimegetattr(), setattr(), hasattr(), delattr()
Inspect an objecttype(), isinstance(), dir(), vars()
Skip the boilerplate@dataclass