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.
__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.
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.159Class 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.
__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.
Private convention
Python has no truly 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.
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.
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.
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.
@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 ().
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
