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__.
def __init__ ( self , x , 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:
Creates a new namespace (a dictionary) for the class body. Executes every top-level statement in that namespace — assignments create class attributes, def statements create class methods, and even arbitrary expressions are evaluated. Calls type(name, bases, namespace) to construct the class object, binding it to the class name in the enclosing scope. print ( " Class body is executing right now " )
## 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.
def __new__ ( cls , * args , ** kwargs ):
print ( f "__new__ called on { cls } " )
instance = super (). __new__ ( cls )
def __init__ ( self , value ):
print ( f "__init__ called on { self } " )
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:
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).
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.
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 ):
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.
species = " Canis familiaris "
def __init__ ( self , name ):
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.
print (a.species) # Wolf (instance dict)
print (b.species) # Canis familiaris (class dict)
print (Dog.species) # Canis familiaris (unchanged)
A class variable through an instance, the mutation is visible to all instances. self .tags.append(tag) # mutates the shared list
print (b.tags) # ['x'] -- surprise
Fix: assign the mutable in __init__.
Python has three kinds of methods, distinguished by the decorators that wrap them.
The default. The descriptor wraps the function so that accessing it on an instance produces a bound Method with self pre-filled.
@classmethod binds the first parameter to the class (not the instance). Used for alternative Constructors and methods that operate on the class rather than instances.
def __init__ ( self , year , month , day ):
def from_iso ( cls , iso_string ):
year, month, day = map ( int , iso_string.split( " - " ))
return cls (year, month, day)
t = datetime.date.today()
return cls (t.year, t.month, t.day)
d = Date.from_iso( " 2025-06-04 " )
print ( type (d). __name__ ) # Date
The cls parameter ensures that subclass constructors return instances of the subclass, not the Base class. This is the primary advantage over static methods for factory patterns.
@staticmethod wraps a function without binding any first parameter. It is a namespace tool — a Way to attach utility functions to a class for organizational purposes.
def clamp ( value , lo , hi ):
return max (lo, min (value, hi))
Static methods receive no implicit arguments. They cannot access self or cls. If a method does Not need either, making it static is a signal to readers and static analysis tools.
Be overridden in a subclass and dispatch to the correct class via `cls`. A static method cannot -- It is a plain function that happens to live in a class namespace.@property turns a method into a managed attribute. It is the Pythonic replacement for explicit Getter/setter methods. The key advantage: you can start with a plain attribute and promote it to a Property later without changing the public API.
def __init__ ( self , celsius ):
return self .celsius * 9 / 5 + 32
def fahrenheit ( self , value ):
self .celsius = (value - 32 ) * 5 / 9
raise AttributeError ( " Cannot delete fahrenheit " )
Under the hood, @property creates a descriptor (discussed later) that intercepts attribute Access on the class. The property object has fget``fsetAnd fdel attributes corresponding to The getter, setter, and deleter functions.
print (t.fahrenheit) # 212.0
Properties with only a getter (no setter) are read-only from the perspective of external code. Attempting to assign to them raises AttributeError. This is the standard way to create computed Attributes and enforce invariants.
def __init__ ( self , radius ):
return math.pi * self .radius ** 2
raise ValueError ( " Radius must be positive " )
Python supports single and multiple inheritance. Every class implicitly inherits from object if no Base classes are specified.
def __init__ ( self , name ):
raise NotImplementedError
return f " { self .name } says Woof"
return f " { self .name } says Meow"
When you access an attribute on an instance, Python searches through the class hierarchy in a Specific order called the Method Resolution Order . You can inspect it with ClassName.__mro__ Or ClassName.mro().
## (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
Python 2.2 used a depth-first, left-to-right traversal for MRO. This produced unintuitive results With diamond inheritance patterns and was inconsistent with monotonicity (a property requiring that The order of base classes is preserved and that subclasses respect the order of their parents).
Python 2.3 adopted C3 linearization , an algorithm originally developed for Dylan. C3 satisfies Three constraints:
Monotonicity: If class A appears before class B in the linearization of C, then A appears before B in the linearization of every subclass of C.Consistent local precedence order: If a class directly inherits from both B and C (in that order), then B appears before C in the linearization.Extended precedence graph (EPG) consistency: The linearization must be consistent with the “is-a” relationships implied by the inheritance graph.The algorithm works as follows. Given a class C with direct bases B1, B2, …, Bn:
Start with the list L = [C] + merge(L(B1), L(B2), …, L(Bn), [B1, B2, …, Bn]). The merge operation selects the first head of each list that is not in the tail of any other list, appends it to the result, and removes it from all lists. If no valid head exists, the inheritance graph is inconsistent and Python refuses to create the class. # This raises TypeError: Cannot create a consistent method resolution order (MRO)
class A ( X , Y ): pass # X appears before Y in bases, but Y is a subclass of X
graph TD
D --> B
D --> C
B --> A
C --> A
A --> object
style D fill:#4a90d9,color:#fff
style B fill:#6ba368,color:#fff
style C fill:#d94a4a,color:#fff
style A fill:#d9a84a,color:#fff
style object fill:#888,color:#fff The MRO for D in this diamond is: D -> B -> C -> A -> object. The super() function (discussed Next) follows this order.
super() returns a proxy object that delegates method calls to the next class in the MRO. In Python 3, calling super() with no arguments inside a method automatically resolves the correct class and Instance.
def __init__ ( self , value ):
print ( f "Base.__init__( { value } )" )
def __init__ ( self , value ):
super (). __init__ (value + 1 )
print ( f "Middle.__init__( { value } )" )
def __init__ ( self , value ):
super (). __init__ (value + 1 )
print ( f "Top.__init__( { value } )" )
super() is critical for cooperative multiple inheritance. Each class in the chain calls super() To ensure that every class’s __init__ is called exactly once, in MRO order. If a class calls a Parent’s method directly (e.g., Base.__init__(self, value)), it breaks the chain and classes Further up the MRO may be skipped.
def __init__ ( self , * args , ** kwargs ):
print ( f "Initializing { type ( self ). __name__} " )
super (). __init__ ( * args, ** kwargs)
class BaseModel ( LoggingMixin ):
def __init__ ( self , id = None ):
def __init__ ( self , name , id = None ):
# Initializing User (printed once, via LoggingMixin in MRO)
Python’s multiple inheritance is powerful but demands discipline. The community convention is to use mixins — small, focused classes that provide a single piece of functionality and are designed To be combined with other classes through inheritance.
A mixin should never be instantiated on its own. It should have no __init__ (or a cooperative one That calls super().__init__()), and it should not hold state. Its purpose is to provide methods That a class can “mix in.”
return json.dumps( self . __dict__ )
def to_csv_row ( self , fields ):
return " , " .join( str ( getattr ( self , f, "" )) for f in fields)
class User ( JsonMixin , CsvMixin ):
def __init__ ( self , name , email ):
u = User( " Alice " , " alice@example.com " )
print (u.to_json()) # {"name": "Alice", "email": "alice@example.com"}
print (u.to_csv_row([ " name " , " email " ])) # Alice,alice@example.com
The convention for inheritance ordering is to list the primary base class last, and mixins before It:
class EnhancedUser ( JsonMixin , CsvMixin , User ):
This ordering ensures that mixin methods can override or wrap the primary class’s methods, and that super() calls propagate through the mixins before reaching the primary class.
Avoid the "diamond of death" pattern where two mixins both call `super().__init__()` but the primary
Class does not account for cooperative initialization. If you use mixins with `__init__`Every Class in the hierarchy must use `super().__init__()` and accept `*args, **kwargs` to pass through Arguments it does not need.The abc (Abstract Base Classes) module provides a way to define interfaces that enforce a contract On subclasses. A class with at least one abstract method cannot be instantiated directly.
from abc import ABC , abstractmethod
return f " { type ( self ). __name__} : area= { self .area() :.2f } , perimeter= { self .perimeter() :.2f } "
Attempting to instantiate Shape directly raises TypeError. Subclasses must implement all Abstract methods before they can be instantiated.
def __init__ ( self , radius ):
return math.pi * self .radius ** 2
return 2 * math.pi * self .radius
print (c.describe()) # Circle: area=78.54, perimeter=31.42
from abc import ABC , abstractmethod
def connection_string ( self ):
ABCs can register virtual subclasses using register()Or define a __subclasshook__ that allows Any class satisfying a structural protocol to be considered a subclass without explicit Registration.
from abc import ABC , abstractmethod
def __subclasshook__ ( cls , C ):
if any ( " close " in B. __dict__ for B in C. __mro__ ):
With this hook, any class that defines a close method is considered a virtual subclass of CloseableEven without inheriting from it. This enables structural typing alongside the nominal Typing of traditional inheritance.
Dunder (double underscore) methods are Python’s protocol for operator overloading and integration With built-in functions. They are how user-defined classes participate in Python’s data model.
def __init__ ( self , x , y ):
return f "Point( { self .x !r } , { self .y !r } )"
return f "( { self .x } , { self .y } )"
__repr__ is for developers — it should be unambiguous and, ideally, produce a string that could Be passed to eval() to reconstruct the object. __str__ is for end users — it should be Readable. __str__ falls back to __repr__ if not defined.
def __init__ ( self , rank , suit ):
if not isinstance (other, Card):
return ( self .rank, self .suit) == (other.rank, other.suit)
return hash (( self .rank, self .suit))
Returning NotImplemented (not False) when the other operand has an incompatible type allows Python to try the reflected operation on the other operand. Returning False would prevent this Fallback.
And unusable in sets or as dict keys. If you need hashability, you must define `__hash__` Explicitly. The invariant is: if `a == b`Then `hash(a) == hash(b)`. Violating this causes silent Data corruption in sets and dicts. def __getitem__ ( self , index ):
return self ._cards[index]
def __contains__ ( self , card ):
return card in self ._cards
Defining __len__ and __getitem__ makes your class work with len()Indexing, slicing, and Iteration (the for loop falls back to sequential integer indexing if __iter__ is not defined). Defining __iter__ is preferred for custom iteration logic.
def __init__ ( self , factor ):
__call__ makes instances behave like functions. This pattern is used extensively: functools.partial``threading.Thread (which calls the target function), and many decorator Implementations rely on __call__.
def __init__ ( self , name ):
self .start = time.perf_counter()
def __exit__ ( self , exc_type , exc_val , exc_tb ):
elapsed = time.perf_counter() - self .start
print ( f " { self .name } : { elapsed :.4f } s" )
__enter__ is called when the with block is entered. Its return value is bound to the variable After as. __exit__ is called when the block exits, whether normally or via exception. If __exit__ returns TrueThe exception is suppressed. The contextlib module provides @contextmanager for simpler cases where a function-based approach is cleaner.
The @dataclass decorator (Python 3.7+) automates the generation of __init__``__repr__And __eq__ based on class-level type annotations. It eliminates boilerplate for classes that are Primarily containers for data.
from dataclasses import dataclass
print (p) # Person(name='Alice', age=30, email='unknown')
print (p == Person( " Alice " , 30 , " unknown " )) # True
from dataclasses import dataclass, field
items: list = field( default_factory = list )
The field() function provides fine-grained control:
default_factory: a zero-argument callable that produces the default value. Always use this for mutable defaults.repr=False: exclude from the generated __repr__.compare=False: exclude from __eq__ and __hash__.init=False: do not include in the generated __init__. return hash (( self .x, self .y))
frozen=True makes instances immutable (assigning to attributes raises FrozenInstanceError) and Automatically generates __hash__. Frozen dataclasses are suitable as dict keys and set members.
Python has three overlapping mechanisms for data-holding classes. Each exists for different reasons:
Feature namedtupledataclassattrs (third-party)Mutable No Yes (configurable) Yes (configurable) Typing Optional Built-in Built-in Inheritance Limited Full Full Validation None Manual Built-in Performance Excellent Good Good Stdlib Yes Yes (3.7+) No
namedtuple is the right choice when you need a lightweight, immutable, memory-efficient container with positional access. It is a tuple subclass, so it is compatible with APIs that expect tuples. Its limitation is that you cannot add methods meaningfully or use inheritance beyond the trivial case.dataclass is the right choice for mutable or immutable data containers that need methods, validation in __post_init__Inheritance, or __slots__. It integrates with the type annotation system and generates methods at class definition time.attrs predates dataclass and provides additional features: automatic validation via @attr.ib(validator=...)Automatic conversion, and more sophisticated configuration. dataclass was explicitly designed as a stdlib answer to the most common attrs use cases.The design philosophy: dataclass does not try to replace namedtuple (which serves the tuple Compatibility use case) or attrs (which serves the heavy-weight validation use case). It occupies The middle ground.
By default, every Python object stores its attributes in a per-instance dictionary (__dict__). This provides maximum flexibility but has a memory cost: each empty __dict__ consumes roughly 100-200 bytes of overhead, and dictionary operations have higher constant factors than attribute Access on a fixed-layout object.
__slots__ replaces the per-instance dictionary with a fixed set of attribute names, stored in a Compact array. This reduces memory usage by 40-60% per instance and can improve attribute access Speed.
def __init__ ( self , x , y ):
p.z = 3 # AttributeError: "DensePoint'' object has no attribute "z'
1. Instances cannot have attributes not listed in `__slots__` (no dynamic attribute assignment). 2. Each class in an inheritance hierarchy must define its own `__slots__`. If a base class omits `__slots__`Subclasses gain a `__dict__` regardless. 3. `__slots__` cannot contain `__dict__` or `__weakref__` unless you explicitly add them as strings (which re-enables those features). 4. Code that relies on `__dict__` (e.g., serialization, `vars()`Some ORMs) will break.# No __dict__ -- memory efficient
Descriptors are the underlying mechanism that makes properties, class methods, static methods, and super() work. A descriptor is any object that implements at least one of __get__``__set__Or __delete__.
Data descriptor: defines __get__ and at least one of __set__ or __delete__. Data descriptors take priority over instance __dict__ entries.Non-data descriptor: defines only __get__. Instance __dict__ entries take priority over non-data descriptors.This distinction is crucial. Functions are non-data descriptors, which is why you can shadow a class Method with an instance attribute. Properties are data descriptors, which is why they cannot be Shadowed by instance attributes.
flowchart TD
A["Attribute access: obj.attr"] --> B{"attr in type(obj).__dict__?"}
B -->|Yes| C{"It is a data descriptor?<br/>__set__ or __delete__ defined?"}
C -->|Yes| D["Call descriptor.__get__(obj, type(obj))"]
C -->|No| E{"attr in obj.__dict__?"}
E -->|Yes| F["Return obj.__dict__[attr]"]
E -->|No| G["Call descriptor.__get__(obj, type(obj))"]
B -->|No| E
G --> H{"Raises AttributeError?"}
H -->|Yes| I["Try __getattr__(obj, attr)"]
H -->|No| J["Return result"]
I --> K{"__getattr__ defined?"}
K -->|Yes| L["Call __getattr__(obj, attr)"]
K -->|No| M["Raise AttributeError"]
style D fill:#4a90d9,color:#fff
style F fill:#6ba368,color:#fff
style L fill:#d9a84a,color:#fff
style M fill:#d94a4a,color:#fff def __init__ ( self , validator ):
self .validator = validator
def __set_name__ ( self , owner , name ):
self .attr_name = f "_ { name } "
def __get__ ( self , obj , objtype = None ):
return getattr (obj, self .attr_name)
def __set__ ( self , obj , value ):
self .validator( self .attr_name, value)
setattr (obj, self .attr_name, value)
name = Validated( lambda attr , v : isinstance (v, str ) and len (v) > 0
or print ( f " { attr } : must be non-empty string" ))
age = Validated( lambda attr , v : isinstance (v, int ) and v >= 0
or print ( f " { attr } : must be non-negative integer" ))
def __init__ ( self , name , age ):
__set_name__ (Python 3.6+) is called by the metaclass when the class is created, giving the Descriptor knowledge of the attribute name it was assigned to. This eliminates the need to pass the Name as a string argument.
A plain function is a non-data descriptor. Its __get__ method returns a bound method object when Accessed on an instance:
# Roughly equivalent to what function.__get__ does:
def function_get ( func , obj , objtype = None ):
return lambda * args , ** kwargs : func(obj, * args, ** kwargs)
When you write obj.method()Python:
Looks up method on type(obj). Finds a function (a non-data descriptor). Calls function.__get__(obj, type(obj))Which returns a bound method. Calls the bound method with the arguments you provided. This is the complete explanation for why self is necessary: the descriptor protocol supplies the Instance, and the function’s signature receives it.
property is a data descriptor:
def __init__ ( self , fget = None , fset = None , fdel = None , doc = None ):
def __get__ ( self , obj , objtype = None ):
raise AttributeError ( " unreadable attribute " )
def __set__ ( self , obj , value ):
raise AttributeError ( " can't set attribute " )
Because property defines __set__It is a data descriptor and takes priority over instance __dict__. This is why you cannot bypass a property setter by assigning directly to an instance Attribute — the descriptor intercepts the assignment.
from dataclasses import dataclass, field
from abc import ABC , abstractmethod
@dataclass ( slots = True , eq = False )
class Account ( Validated ):
_transactions: list = field( default_factory = list )
def deposit ( self , amount ):
raise ValueError ( " Deposit must be positive " )
self ._transactions.append(( " deposit " , amount))
def withdraw ( self , amount ):
raise ValueError ( " Withdrawal must be positive " )
if amount > self .balance:
raise ValueError ( " Insufficient funds " )
self ._transactions.append(( " withdrawal " , amount))
raise ValueError ( " Owner is required " )
raise ValueError ( " Balance cannot be negative " )
return f "Account( { self .owner } , balance= { self .balance :.2f } )"
return f "Account(owner= { self .owner !r } , balance= { self .balance !r } )"
if not isinstance (other, Account):
return ( self .owner, self .balance) == (other.owner, other.balance)
return hash (( self .owner, self .balance))
return len ( self ._transactions)
return iter ( self ._transactions)
def __getitem__ ( self , index ):
return self ._transactions[index]
This class combines dataclasses (for boilerplate reduction), ABCs (for interface enforcement), slots (for memory efficiency), and multiple dunder methods (for full Python data model integration). Each Of these mechanisms addresses a separate concern, and they compose without conflict.
A class is a blueprint, and each instance is a house built from that blueprint. The blueprint itself is not a house — it is a plan that tells you what rooms to build. When you call __init__, you are furnishing the house with specific furniture. self is the address of the house — every method needs to know which house it is working on. Inheritance is like extending a blueprint: a Dog blueprint adds barking to the Animal blueprint. Multiple inheritance is like combining blueprints from two parents — powerful but risky if both blueprints define the same room differently. Descriptors are the magic behind properties — they intercept attribute access and can compute values on the fly, turning a simple attribute into a gatekeeper.
Misunderstanding the difference between a stack (LIFO) and a queue (FIFO) in data structure applications.
Forgetting edge cases in algorithm design (e.g., empty input, single element, already sorted data).
Neglecting to normalise database designs, leading to data redundancy and update anomalies.
Mixing up Big O, Big Ω \Omega Ω , and Big Θ \Theta Θ notation. Big O is an upper bound, not necessarily tight.
The key principles covered in this topic are linked in the sub-pages above. Focus on understanding the definitions, applying the formulas or frameworks, and evaluating strengths and limitations of each approach.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
## 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.