Classes and objects

Every type you have worked with so far (strings, lists, dictionaries) is actually a class. When you call "hello".upper(), you are calling a method on a string object. Classes let you define your own types with their own data and behaviour. A Player class can hold a name, a score, and a level, and know how to display itself.
Blueprint and instances
A class is a blueprint. An instance is a specific thing made from that blueprint. You can make as many instances as you need, each with its own data but sharing the same methods defined in the class.
class Dog:
def bark(self):
print("Woof!")
rex = Dog()
luna = Dog()
rex.bark() # "Woof!"
luna.bark() # "Woof!"Dog is the class. rex and luna are instances: two different dogs, each sharing the same behaviour defined in the class.
Dog(), and you get a fresh instance back. Every instance shares the class's methods but keeps its own data, so rex and luna can behave the same while being separate dogs. __init__ and self
__init__ is the method Python calls automatically when you create a new instance. It is where you set up the starting data for the object. self is how a method refers to the specific instance it is operating on, and it is always the first parameter.
class Player:
def __init__(self, name, score=0):
self.name = name
self.score = score
def add_points(self, points):
self.score += points
def display(self):
print(f"{self.name}: {self.score} points")
alice = Player("Alice")
bob = Player("Bob", score=50)
alice.add_points(30)
alice.display() # "Alice: 30 points"
bob.display() # "Bob: 50 points"self.name and self.score are instance attributes: they belong to the specific object, not the class itself. Each Player instance has its own name and score.
__init__ runs the moment you create an instance, so it is where you set the starting data with self.name = value. self is the instance Python is working on, and it is always the first parameter of a method, handed to you automatically. You never pass self yourself when you call alice.display(). Methods
Any function defined inside a class is a method. Instance methods always have self as the first parameter; Python passes it automatically. Methods can read and change the instance's data via self.
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def scale(self, factor):
self.radius *= factor
return self # returning self allows chaining: c.scale(2).scale(0.5)
c = Circle(5)
print(c.area()) # 78.53975
c.scale(2)
print(c.area()) # 314.159self, the instance it is working on. Python passes self for you, so you call c.area() with nothing extra. Through self a method reads and changes that object's own data. Class variables vs instance variables
Variables defined directly on the class (not inside __init__) are class variables. All instances share the same class variable. Variables set on self inside __init__ are instance variables, unique to each object.
class Player:
max_lives = 3 # class variable, same for every Player
def __init__(self, name):
self.name = name # instance variable, unique to each Player
self.lives = Player.max_lives
def die(self):
self.lives -= 1
alice = Player("Alice")
bob = Player("Bob")
Player.max_lives = 5 # change for all current and future instancesUse class variables for values shared across all instances: constants, counters, defaults. Use instance variables for data that differs per object.
self in __init__ belongs to that one object. Writing self.attr = value always makes or updates the instance's own copy. So reach for class variables when a value is the same for everyone, instance variables when it differs per object. __str__ and __repr__
__str__ controls what print() and f-strings show for your object. __repr__ controls the developer view shown in the console and for debugging. Always define __repr__. Define __str__ when you want a clean user-facing display separate from the debug view.
class Player:
def __init__(self, name, score):
self.name = name
self.score = score
def __str__(self):
return f"{self.name} ({self.score} pts)"
def __repr__(self):
return f"Player(name={self.name!r}, score={self.score})"
alice = Player("Alice", 87)
print(alice) # "Alice (87 pts)" (uses __str__)
repr(alice) # "Player(name='Alice', score=87)" (uses __repr__)Always define __repr__. Define __str__ when you want a clean user-facing representation separate from the debug view. If only __repr__ is defined, Python uses it for both.
__str__ is what print() and f-strings show, the friendly version. __repr__ is the developer view you see in the console. Always write __repr__; it is the one that has a job even when you forget __str__. Add __str__ only when the user-facing text should read differently. Private convention
Python has no real private variables, but a single underscore at the start of a name (_balance) is a convention that signals "this is internal, do not use it directly from outside the class". It is not enforced by the language; it is a communication to other developers.
class BankAccount:
def __init__(self, balance):
self._balance = balance # _ means "hands off"
def deposit(self, amount):
if amount > 0:
self._balance += amount
def balance(self):
return self._balanceA double underscore (__name) triggers name mangling; Python renames the attribute to _ClassName__name to avoid conflicts in subclasses. It is rarely needed. Single underscore is the convention in most code.
_balance) is the agreed signal for "internal, leave this alone from outside". Nothing stops you reaching in, it is a message to other developers, including future you. A double underscore is a rarer tool for avoiding name clashes in subclasses; the single underscore is what you will use day to day. Inheritance
A class can inherit from another class, getting all its attributes and methods automatically. You can then override specific methods in the subclass to change their behaviour. This lets you reuse a common base and specialise where needed.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
pets = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for pet in pets:
print(pet.speak())Dog and Cat inherit __init__ from Animal, so they do not need their own. They override speak() with their specific behaviour.
Dog and Cat reuse Animal's __init__ and only redefine speak(). super()
super() calls a method from the parent class. Use it when you want to extend the parent's behaviour rather than replace it entirely: call the parent's __init__ to run its setup, then add anything your subclass needs on top.
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
class Dog(Animal):
def __init__(self, name):
super().__init__(name, "Woof") # call Animal.__init__
self.tricks = [] # add something extra
def learn(self, trick):
self.tricks.append(trick)
rex = Dog("Rex")
rex.learn("sit")
print(rex.tricks) # ["sit"]Always call super().__init__() when your subclass has its own __init__ and the parent does too.
super() reaches the parent class, so super().__init__() runs the parent's setup before you add your own. Use it when your subclass writes its own __init__ and the parent has one too. Skip it and the parent's setup never runs, leaving the object half-built. Class methods and static methods
@classmethod creates a method that receives the class itself instead of an instance. It is useful for alternative constructors: creating an instance from a string, a file, or another format. @staticmethod is a plain function that lives inside the class for organisational reasons; it receives neither the instance nor the class.
class Player:
def __init__(self, name, score):
self.name = name
self.score = score
@classmethod
def from_string(cls, data):
name, score = data.split(",")
return cls(name, int(score))
alice = Player.from_string("Alice,87")class Player:
@staticmethod
def is_valid_name(name):
return name.isalpha() and len(name) >= 2
Player.is_valid_name("Alice") # True
Player.is_valid_name("A1") # FalseUse @classmethod for alternative constructors. Use @staticmethod for utility functions that logically belong with the class but do not need instance or class data.
@classmethod hands you the class instead of an instance, which makes it the go-to for alternative constructors: build a Player from a string, a file, whatever format you have. @staticmethod is an ordinary function tucked inside the class for tidiness; it gets neither the class nor the instance. Plain methods touch one object's data, these two don't. @property
@property lets you access a method like an attribute, with no parentheses needed. Use it for values that are computed from other attributes and feel natural to read as simple attribute access.
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
@property
def diameter(self):
return self.radius * 2
c = Circle(5)
print(c.area) # 78.53975 (looks like an attribute, runs like a method)
print(c.diameter) # 10Properties are useful for computed values: things derived from other attributes that feel natural to access without ().
@property lets you read a method like an attribute, no parentheses: c.area instead of c.area(). It fits values that are worked out from other attributes and feel natural to read as plain data. Behind the scenes it still runs your method every time you access it. In practice
A Player class with instance attributes, methods, a @property, and __str__:
class Player:
max_lives = 3
def __init__(self, name: str):
self.name = name
self.score = 0
self.lives = Player.max_lives
def earn_points(self, amount: int) -> None:
self.score += amount
def take_hit(self) -> bool:
self.lives -= 1
return self.lives > 0
@property
def is_alive(self) -> bool:
return self.lives > 0
def __str__(self) -> str:
return f"{self.name} | Score: {self.score} | Lives: {self.lives}"
alice = Player("Alice")
alice.earn_points(50)
alice.take_hit()
print(alice) # "Alice | Score: 50 | Lives: 2"
print(alice.is_alive) # True
