Filter concepts by levelShowing all levels.

Python · OOP fundamentals

OOP fundamentals

Building blueprints, the objects they build, and the three ways objects relate to each other

  • Class builds objectsName() calls the class, each call is independent
  • __init__ sets up selfruns automatically right after construction
  • class Sub(Base):inherits everything; super() reaches the parent
  • Is-a vs has-ainheritance for stable kinds, composition for parts

1.Classes and objects

A class defines the shape something has — its attributes and behaviour. An object is one concrete thing built from that shape. class Dog: defines the blueprint; Dog() builds an object from it.

Key features

  • class Name: defines a blueprint — it runs no instance-specific code by itself
  • Name() calls the class, which builds and returns one new object
  • Each object built this way has its own independent set of attributes
  • type(obj) reports the class that built an object; isinstance(obj, Name) checks it
  • A class is itself an object too — of type type — which is why Name() can be called

Class vs. the objects it builds

Class vs. the objects it builds
ExpressionWhat it is
class Dog: ...the blueprint — defines what every Dog will have
Dog()one new object, built from the blueprint
type(rex)<class '__main__.Dog'> — the class that built rex
isinstance(rex, Dog)True — rex was built by the Dog class
rex is fidoFalse — two calls to Dog() build two distinct objects
One blueprint, many independent objects

class Dog

the blueprint — no dog yet

Dog()

called twice, independently

rex, fido

two distinct objects, same shape

  1. class Dog — the blueprint — no dog yet
  2. Dog() — called twice, independently
  3. rex, fido — two distinct objects, same shape

Command

python
class ClassName:
    """A blueprint — this body defines shape, not one instance."""
    ...

obj = ClassName()   # calling the class builds one object

A class is the blueprint; Name() builds one independent object from it. type(obj) and isinstance(obj, Name) both ask which blueprint built it.

2.Constructors

def __init__(self, ...): is the method Python calls automatically right after building a new object, to set up its starting attributes. Dog("Rex") builds the object, then calls __init__(new_dog, "Rex") for you.

Key features

  • __init__(self, ...) is called automatically right after Name(...) builds the object
  • self is the newly built object — the first parameter, always, by convention named self
  • __init__ sets up attributes; it never returns a value (returning non-None is a TypeError)
  • Arguments passed to Name(...) are the arguments __init__ receives, after self
  • __new__, not __init__, is what actually constructs the object — rarely overridden

What happens when Dog("Rex", 3) runs

What happens when Dog("Rex", 3) runs
StepWhat happens
1. __new__a new, empty Dog object is allocated (default: object.__new__)
2. __init__Python calls __init__(new_obj, "Rex", 3) on that object
3. inside __init__self.name = "Rex" and self.age = 3 set its attributes
4. resultDog("Rex", 3) evaluates to the now-initialized object
Build, then initialize

Dog("Rex", 3)

the call

__new__

allocates an empty object

__init__(self, ...)

sets self.name, self.age

  1. Dog("Rex", 3) — the call
  2. __new__ — allocates an empty object
  3. __init__(self, ...) — sets self.name, self.age

Command

python
class ClassName:
    def __init__(self, arg1, arg2):
        self.attr1 = arg1
        self.attr2 = arg2

obj = ClassName(value1, value2)  # __init__ runs automatically

__init__(self, ...) runs automatically right after Name(...) builds the object — it sets up attributes on self and must return None.

3.Inheritance

class Puppy(Dog): makes Puppy inherit every attribute and method Dog defines. Puppy can add new ones, or override an inherited one by redefining it — and super() reaches up to call the parent version from inside the override.

Key features

  • class Sub(Base): makes Sub inherit every attribute and method Base defines
  • An overridden method in Sub is used instead of the Base version, for Sub instances
  • super().method(...) calls the parent class's version — most often inside an override
  • isinstance(obj, Base) is True for a Sub instance too — Sub "is a" Base
  • A class's Method Resolution Order (Cls.__mro__) is the path used to look up an attribute

What a subclass gains, keeps, and can change

What a subclass gains, keeps, and can change
ActionEffect
class Puppy(Dog): passPuppy has every Dog attribute and method, unchanged
def bark(self): ... in Puppyoverrides Dog.bark for Puppy instances only
super().bark()calls Dog's original bark from inside Puppy's override
isinstance(pup, Dog)True — a Puppy is-a Dog
Puppy.__mro__(Puppy, Dog, object) — the lookup order for attributes
A subclass extends, and can override, its base

class Dog

defines bark(), __init__

class Puppy(Dog)

inherits both, unchanged

override bark()

super().bark() reaches the original

  1. class Dog — defines bark(), __init__
  2. class Puppy(Dog) — inherits both, unchanged
  3. override bark() — super().bark() reaches the original

Command

python
class Base:
    def method(self):
        return "base"

class Sub(Base):
    def method(self):              # overrides Base.method
        return super().method() + " + sub"

class Sub(Base): inherits everything; overriding __init__ replaces the parent one entirely unless the override calls super().__init__(...) explicitly.

4.Instance attributes

An instance attribute is data that belongs to one specific object — self.name = value inside __init__ (or any method) creates or updates it on that object alone. Two instances of the same class hold two separate copies.

Key features

  • self.attr = value, usually in __init__, creates the attribute on that one instance
  • Each instance's attributes live in its own __dict__ — completely independent
  • Reading self.attr checks the instance first, then falls back to the class attribute
  • Attributes can be added, changed, or deleted after __init__ too — nothing locks them
  • vars(obj) or obj.__dict__ shows exactly what that instance's attributes are

Working with instance attributes

Working with instance attributes
CodeEffect
self.name = "Rex"creates/overwrites name on this instance only
rex.age = 3attributes can be set from outside __init__ too
del rex.ageremoves age from rex — AttributeError on the next read
vars(rex){'name': 'Rex'} — this instance's own attribute dict
getattr(rex, "age", 0)0 — safe read with a fallback if age isn't set
Two instances, two separate __dict__ notebooks

class Dog

shared code — __init__, methods

rex.__dict__

{'name': 'Rex', 'age': 3}

fido.__dict__

{'name': 'Fido'} — age was never set here

  1. class Dog — shared code — __init__, methods
  2. rex.__dict__ — {'name': 'Rex', 'age': 3}
  3. fido.__dict__ — {'name': 'Fido'} — age was never set here

Command

python
class ClassName:
    def __init__(self, value):
        self.attr = value   # instance attribute, set at construction time

obj = ClassName(1)
obj.other = 2                # can also be set after construction

self.attr = value writes to ONE instance's own __dict__ — instances of the same class never share attributes unless the class arranges it.

5.Class attributes

A class attribute is defined directly in the class body (not inside __init__) and lives on the class object itself, shared by every instance that does not set its own attribute of the same name.

Key features

  • Defined in the class body, outside any method: species = "Canis familiaris"
  • Shared by every instance — one value, looked up on the class, not copied per instance
  • self.attr = value on an instance ALWAYS creates a new attribute, never edits the class one
  • Lookup order: instance __dict__ first, then class __dict__ (see class methods)
  • Best suited to a value constant across instances — a shared counter, or a default value

Reading and writing a class attribute

Reading and writing a class attribute
CodeEffect
class Dog: species = "Canis familiaris"defines a class attribute, shared by every Dog
rex.speciesreads the class attribute — rex has no instance override
rex.species = "Wolf"creates an INSTANCE attribute on rex only — the class value is untouched
Dog.species = "Wolf"changes the class attribute itself — visible to every instance without one of its own
Dog.speciesreads the class attribute directly, no instance involved

Command

python
class ClassName:
    shared_value = 0        # class attribute — one copy, on the class

    def __init__(self, value):
        self.own_value = value   # instance attribute — one copy per instance

self.attr = value always creates an instance attribute — to change a shared class attribute, assign through the CLASS (Dog.attr = value), not an instance.

6.Instance methods

An instance method is a function defined inside a class body that takes self as its first parameter. rex.bark() automatically passes rex as self, so the method can read and change that specific instance's attributes.

Key features

  • def method(self, ...): inside a class body defines an instance method
  • obj.method(args) automatically passes obj as self — never written explicitly at the call
  • self gives the method access to that instance's attributes and other methods
  • Calling Class.method(obj, args) is equivalent, and shows what obj.method(args) really does
  • Without a call through an instance (or an explicit class call) self has nothing to bind

How an instance method call is really dispatched

How an instance method call is really dispatched
CallWhat actually runs
rex.bark()Dog.bark(rex) — rex is bound to self automatically
Dog.bark(rex)the same call, spelled out explicitly
rex.rename("Max")Dog.rename(rex, "Max") — extra arguments pass through after self
Dog.bark<function Dog.bark at 0x...> — an ordinary function, before binding
rex.bark<bound method Dog.bark of <Dog object>> — self already attached
obj.method() binds self automatically

rex.bark()

called through an instance

self = rex

bound automatically

Dog.bark(rex)

what actually runs

  1. rex.bark() — called through an instance
  2. self = rex — bound automatically
  3. Dog.bark(rex) — what actually runs

Command

python
class ClassName:
    def method_name(self, arg):
        return self.attr + arg   # self gives access to this instance

obj.method_name(value)   # self is bound to obj automatically

obj.method(args) is Class.method(obj, args) — self is the instance the call went through, bound automatically, never passed explicitly.

7.Class methods

@classmethod marks a method whose first parameter is the class itself (cls), not an instance. Called as Dog.from_string(...) or rex.from_string(...), Python binds cls automatically — most often used for an alternative constructor.

Key features

  • @classmethod, then def name(cls, ...): — cls is bound automatically, like self
  • cls is the class, whether called as Dog.name(...) or through an instance, rex.name(...)
  • cls(...) inside the method builds a new instance — the "alternative constructor" pattern
  • Receiving cls rather than a hardcoded class name means it works for subclasses too
  • Unlike a static method, a class method always has the class available, no instance needed

Instance method vs. class method vs. static method (preview)

Instance method vs. class method vs. static method (preview)
KindFirst parameterCalled as
instance methodself (the instance)rex.bark()
class methodcls (the class)Dog.from_string(...) or rex.from_string(...)
static methodneitherDog.helper() or rex.helper()

Command

python
class ClassName:
    @classmethod
    def alt_constructor(cls, raw_value):
        return cls(parse(raw_value))   # builds an instance via cls, not the class name

@classmethod binds cls (the class) as the first parameter — use it to build alternative constructors with cls(...) so subclasses build correctly.

8.Static methods

@staticmethod marks a method with neither self nor cls — it behaves like a plain function, just namespaced inside the class because it is logically related to it. Nothing is bound automatically at the call site.

Key features

  • @staticmethod, then def name(args): — no self, no cls, ever
  • Called through the class or an instance identically: Dog.helper(x) or rex.helper(x)
  • Cannot read or write instance or class state directly — no self or cls to do it with
  • Used for a function that is conceptually related to the class but does not need its data
  • The only real difference from a plain function is where it lives — the class namespace

The three method kinds, side by side

The three method kinds, side by side
DecoratorFirst parameterCan access
(none)selfthis instance's attributes, and the class via type(self)
@classmethodclsthe class and its attributes — no specific instance
@staticmethod(none)only what is passed in as arguments

Command

python
class ClassName:
    @staticmethod
    def helper(x, y):
        return x + y   # no self, no cls — just the arguments given

@staticmethod methods take no self or cls — they behave like a plain function, just namespaced inside the class for organization.

9.Polymorphism

Polymorphism means calling the same method name — shape.area() — on objects of different types and getting each type's own correct behaviour, without the caller ever checking which type it actually has.

Key features

  • Different classes can implement the same method name, each with its own behaviour
  • A caller can call obj.method() without checking type(obj) — each type answers for itself
  • Python's polymorphism is duck typing — no shared base class needed, just a shared method
  • Overriding a method (see inheritance) is one way to get it, but not the only way
  • "Walks like a duck, quacks like a duck" — Python checks behaviour, not declared type

The same call, different types, different results

The same call, different types, different results
CallResult
Circle(2).area()12.566... — πr²
Rectangle(3, 4).area()12 — width × height
for s in shapes: s.area()each shape computes its own area — no type check needed
len("abc"), len([1,2]), len({1:2})one function, three unrelated types, each defines __len__

Command

python
def total_area(shapes):
    return sum(shape.area() for shape in shapes)   # no isinstance() check needed

Polymorphism: calling obj.method() and letting each type answer for itself — no isinstance()/type() branching, and no shared base class required (duck typing).

10.Composition

Composition builds a class out of other objects it creates and owns — a Car creates its own Engine inside __init__. The Engine's lifetime is entirely tied to the Car's: destroy the car, and its engine goes with it.

Key features

  • "Has-a", not "is-a" — a Car has an Engine; it is not a kind of Engine
  • The owning object typically creates the part itself, inside its own __init__
  • The part's lifetime is tied to the whole — no other object holds a reference to share it
  • Composition favours delegation over inheritance: Car.start() calls self.engine.start()
  • Preferred over inheritance when the relationship is "built from," not "a kind of"

Composition: the whole owns and creates the part

Composition: the whole owns and creates the part
CodeRelationship
self.engine = Engine()Car creates its own Engine in __init__ — ownership starts here
self.engine.start()Car delegates to the part it owns, rather than reimplementing start logic
del carcar's engine has no other owner — nothing else was holding a reference to it
Car "has-a" Enginecontrast with Puppy "is-a" Dog (inheritance)

Command

python
class Whole:
    def __init__(self):
        self.part = Part()        # created here, owned by Whole

    def do_thing(self):
        return self.part.do_thing()   # delegates to the owned part

"Has-a", built and owned by the whole: self.part = Part() inside __init__. The part's lifetime is tied to the owner — nothing else holds a reference to it.

11.Aggregation

Aggregation is a "has-a" relationship where the whole holds a reference to a part it did NOT create — Team(players) stores players built and passed in from outside. The players exist independently and outlive the team if it is discarded.

Key features

  • "Has-a", like composition — but the part is created OUTSIDE and merely referenced
  • Passed in via a constructor or setter — not built inside __init__ like composition
  • The part can be shared across multiple wholes at once (one Player on two Teams)
  • The part outlives the whole — discarding a Team does not discard its Players
  • Weaker coupling than composition: share or swap a part freely, without managing its life

Aggregation vs. composition — who creates and owns the part

Aggregation vs. composition — who creates and owns the part
QuestionComposition (Car/Engine)Aggregation (Team/Player)
Who creates the part?the whole, internallycreated externally, passed in
Can the part be shared?no — one owner onlyyes — one Player, many Teams
Does the part outlive the whole?no — tied to its lifetimeyes — independent lifetime

Command

python
class Team:
    def __init__(self, players):
        self.players = players   # accepted from outside, not built here

Aggregation: self.part = part, where part was built OUTSIDE — the whole references it but never owns its lifetime, and it can be shared or outlive the whole.

12.Composition vs inheritance

Inheritance models "is-a" through a fixed class hierarchy; composition models "has-a" by holding and delegating to other objects. "Favour composition over inheritance" means default to composition unless "is-a" is genuinely true and stable.

Key features

  • Inheritance: "is-a", fixed at class-definition time, one class extends exactly one shape
  • Composition: "has-a", assembled from parts that can be swapped at runtime
  • A deep inheritance hierarchy is brittle — a base-class change can ripple through it all
  • Composition mixes and matches behaviour without multiplying subclasses per combination
  • "Favour composition" is a default, not absolute — a stable is-a still suits inheritance

Which one fits — the "is-a" vs "has-a" test

Which one fits — the "is-a" vs "has-a" test
Question to askIf yes
Is a Duck really a kind of Bird, permanently?inheritance may fit — genuine "is-a"
Does a Duck need to fly, swim, AND quack, but a Penguin only swim and quack?composition — mix in only the behaviours each needs
Would a new combination require yet another subclass?composition — swap parts instead of multiplying classes
Is the relationship "uses" or "is built from", not "is a specialized kind of"?composition (or association) — see those concepts
The combinatorial explosion inheritance can cause

Bird

base class

FlyingBird, SwimmingBird...

one subclass per COMBINATION

composition instead

mix independent behaviours per instance

  1. Bird — base class
  2. FlyingBird, SwimmingBird... — one subclass per COMBINATION
  3. composition instead — mix independent behaviours per instance

Command

python
class Duck:
    def __init__(self, fly_behavior, swim_behavior):
        self.fly_behavior = fly_behavior     # composed in, swappable
        self.swim_behavior = swim_behavior

    def perform_fly(self):
        return self.fly_behavior.fly()       # delegates, doesn't inherit

"Is-a" and stable → inheritance. "Has-a", or several independent things that vary → composition, to avoid a subclass explosion.

Remember

  • __init__(self, ...) runs automatically right after Name(...) builds the object — it must return None.
  • self.attr = value always creates or edits an INSTANCE attribute — assign through the class to edit a shared one.
  • Overriding __init__ replaces the parent version entirely unless the override calls super().__init__(...).
  • "Is-a" and stable → inherit. "Has-a", or several independent variations → compose, to avoid a subclass explosion.

Pro tip

  • A @classmethod alternative constructor should call cls(...), never the literal class name, so subclasses build correctly.
  • @staticmethod is for logic that touches neither self nor cls — if it needs either, it is the wrong decorator.
  • Double-underscore name mangling (__name → _Class__name) avoids subclass collisions — it is not real privacy.
  • Trust polymorphism over isinstance()/type() branching — let each object answer obj.method() for itself.

Next up

  • Abstract classes and interfaces
  • Properties
  • Dunder methods

Sources

  • Classes — docs.python.org/3/tutorial/classes.html
  • classmethod/staticmethod — docs.python.org/3/library/functions.html
  • Every example executed on Python 3.14.3 · checked 2026-08-20

Spotted a mistake on this page? Tell us.