Python Classes and OOP Basics: Attributes, Methods, and __init__
__init__runs automatically when a class is called to create a new instance; it sets up that instance's starting state.selfis the instance itself, passed automatically as the first argument to every instance method.- Instance attributes belong to one object; class attributes are shared by every instance unless overridden.
- Dunder methods like
__repr__and__eq__let your objects work with built-in functions and operators.
Defining a class
A class is a blueprint for creating objects. Define one with the class keyword and a body of attributes and methods:
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
b = Book("Dune", "Frank Herbert", 412)
print(b.title) # Dune
Calling Book(...) does two things behind the scenes: Python creates a new, empty object, then calls __init__ on it with the arguments you supplied. The object itself is what self refers to inside the method.
__init__ and self
__init__ is not a constructor in the strict sense — the object already exists by the time __init__ runs — it is an initialiser that sets up the object's starting attributes. self must be the first parameter of every instance method, though Python supplies it automatically at call time; you never pass it explicitly:
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def summary(self):
return f"{self.title} ({self.pages} pages)"
b = Book("Dune", 412)
print(b.summary()) # Dune (412 pages)
Inside summary, self.title and self.pages refer back to the specific object the method was called on — b in this case.
Instance attributes vs class attributes
Attributes set on self inside __init__ belong to that one instance. Attributes defined directly in the class body, outside any method, are shared by all instances until an instance sets its own attribute of the same name:
class Book:
library_name = "City Library" # class attribute, shared
def __init__(self, title):
self.title = title # instance attribute, per-object
a = Book("Dune")
b = Book("Emma")
print(a.library_name, b.library_name) # City Library City Library
a.library_name = "Uptown Branch" # creates an instance attribute on a
print(a.library_name, b.library_name) # Uptown Branch City Library
Reassigning a.library_name does not change the class attribute — it creates a new instance attribute on a that shadows it. b is unaffected. This distinction matters most with mutable class attributes like lists: appending to a shared list class attribute affects every instance, which is rarely what you want.
Writing methods
A method is just a function defined inside a class body. Beyond reading and setting attributes, methods can call other methods on self, and can return new values or modify state:
class Book:
def __init__(self, title, pages, pages_read=0):
self.title = title
self.pages = pages
self.pages_read = pages_read
def read(self, pages):
self.pages_read = min(self.pages, self.pages_read + pages)
def percent_complete(self):
return round(100 * self.pages_read / self.pages, 1)
b = Book("Dune", 412)
b.read(103)
print(b.percent_complete()) # 25.0
Common dunder methods
Methods with double underscores on both sides ("dunder" methods) hook your class into Python's built-in behaviour. __repr__ controls how an object prints in a REPL or inside a container; __eq__ controls what == does between two instances:
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __repr__(self):
return f"Book({self.title!r}, {self.pages})"
def __eq__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.title == other.title and self.pages == other.pages
print(Book("Dune", 412)) # Book('Dune', 412)
print(Book("Dune", 412) == Book("Dune", 412)) # True
Without a custom __eq__, two separate instances compare unequal even with identical attributes, because the default comparison checks object identity, not content. Returning NotImplemented — rather than False — for a mismatched type lets Python fall back to the other object's comparison logic instead of asserting inequality outright.
Properties: computed attributes
A @property lets a method be accessed with plain attribute syntax — no parentheses — which is useful for a value that is computed from other attributes rather than stored directly:
class Book:
def __init__(self, title, pages_read, pages):
self.title = title
self.pages_read = pages_read
self.pages = pages
@property
def percent_complete(self):
return round(100 * self.pages_read / self.pages, 1)
b = Book("Dune", 103, 412)
print(b.percent_complete) # 25.0, no parentheses needed
percent_complete is never stored — it is recalculated from pages_read and pages every time it is read, so it can never silently go stale the way a manually updated attribute could. A matching @name.setter method can be added if you also want assignment through the same attribute name to run custom validation logic, rather than setting the underlying value directly.
classmethod and staticmethod
A regular method always receives the instance as its first argument. @classmethod instead receives the class itself, which is useful for alternative constructors; @staticmethod receives neither, for a function that logically belongs with the class but needs no access to instance or class state:
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
@classmethod
def from_csv_row(cls, row):
title, pages = row.split(",")
return cls(title, int(pages))
@staticmethod
def is_valid_title(title):
return bool(title.strip())
b = Book.from_csv_row("Dune,412")
print(b.title, b.pages) # Dune 412
print(Book.is_valid_title(" ")) # False
from_csv_row is called on the class itself, not an existing instance, and cls(title, int(pages)) creates a new one — this pattern gives a class multiple named ways to construct an object, alongside the single __init__ signature.