Skip to content

Classes and Inheritance

In Python, a class is created with the class keyword. A class is itself an object — an instance Of type. The body of a class statement executes at definition time (when the module is imported Or the function containing it is called), and the resulting namespace dictionary becomes the class”s __dict__.

class Point:
"""A simple 2D point."""
def __init__(self, x, y):
self.x = x
self.y = y
def distance_to(self, other):
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5

The class statement does three things:

  1. Creates a new namespace (a dictionary) for the class body.
  2. Executes every top-level statement in that namespace — assignments create class attributes, def statements create class methods, and even arbitrary expressions are evaluated.
  3. Calls type(name, bases, namespace) to construct the class object, binding it to the class name in the enclosing scope.
class Tracer:
print("Class body is executing right now")
def method(self):
pass
## Output when the module loads: "Class body is executing right now"

This means class bodies are not inert declarations. They are executable code. This property is The foundation of metaclasses, class decorators, and many advanced patterns.

__init__ is the initializer, not the constructor. The actual constructor is __new__A class Method on type that allocates the instance. __init__ receives the already-allocated instance and Populates it.

class Demo:
def __new__(cls, *args, **kwargs):
print(f"__new__ called on {cls}")
instance = super().__new__(cls)
return instance
def __init__(self, value):
print(f"__init__ called on {self}")
self.value = value

Python requires the instance to be passed explicitly as the first parameter of instance methods. The Parameter is conventionally named selfThough the language does not enforce this name — any Valid identifier works.

This is a deliberate design choice with several consequences:

  1. Explicit is better than implicit. If self were implicit, a method’s free variables would include an implicitly-bound name that shadows any outer variable with the same name. By making self an explicit parameter, the binding is always visible at the call site (even if the caller does not write it — the interpreter inserts it automatically when using dotted access).

  2. Methods are just functions. A method and a standalone function share the exact same calling convention. The only difference is that obj.method(args) is syntactic sugar for type(obj).method(obj, args). This means you can pass methods as first-class objects, assign functions to class attributes to turn them into methods, and unbind methods from instances — all without any special machinery.

  3. Uniformity with cls. Class methods explicitly receive the class as their first parameter. Static methods receive nothing. All three cases follow the same rule: the first parameter is whatever the descriptor protocol provides. An implicit this would require a special case for every binding type.

def standalone_func(self, x, y):
return x + y
class Hijack:
method = standalone_func
h = Hijack()
print(h.method(1, 2)) # 3 -- a plain function becomes a bound method

Instance variables are stored in each object’s __dict__ and are set inside methods ( __init__). Class variables are stored in the class’s __dict__ and are shared across all Instances.

class Dog:
species = "Canis familiaris"
def __init__(self, name):
self.name = name
a = Dog("Rex")
b = Dog("Buster")
print(Dog.species) # Canis familiaris
print(a.species) # Canis familiaris (found on class)
print(a.name) # Rex (found on instance)

Attribute lookup follows the chain: instance __dict__ then class __dict__ then base classes (following the MRO). Assignment to an attribute through an instance always sets it on the Instance, never on the class.

a.species = "Wolf"
print(a.species) # Wolf (instance dict)
print(b.species) # Canis familiaris (class dict)
print(Dog.species) # Canis familiaris (unchanged)
## Cross-References
  • Metaclasses: Extends class creation by customizing the class creation process itself, building on the class fundamentals covered here.
  • Descriptors: Explains the underlying mechanism that makes properties, class methods, and static methods work.
  • Protocols and Dunder Methods: Provides deeper coverage of the dunder methods that enable Python’s data model integration.
  • Data Validation: Shows how to use dataclasses and validators to ensure data integrity in object-oriented designs.